Jules was unable to complete the task in time. Please review the work done so far and provide feedback for Jules to continue.

This commit is contained in:
google-labs-jules[bot] 2025-06-12 06:20:40 +00:00
parent b26a68956c
commit 15b9048a9c
7 changed files with 977 additions and 804 deletions

View File

@ -4,81 +4,106 @@
<dict> <dict>
<key>ACPI</key> <key>ACPI</key>
<dict> <dict>
<key>Add</key> <array/> <key>Add</key>
<key>Delete</key> <array/> <array>
<key>Patch</key> <array/> <dict><key>Comment</key><string>SSDT-PLUG-ALT: CPU Power Management</string><key>Enabled</key><true/><key>Path</key><string>SSDT-PLUG-ALT.aml</string></dict>
<dict><key>Comment</key><string>SSDT-EC-USBX: Embedded Controller and USB Power</string><key>Enabled</key><true/><key>Path</key><string>SSDT-EC-USBX.aml</string></dict>
<dict><key>Comment</key><string>SSDT-AWAC: Realtime Clock Fix</string><key>Enabled</key><true/><key>Path</key><string>SSDT-AWAC.aml</string></dict>
<dict><key>Comment</key><string>SSDT-RHUB: USB Reset</string><key>Enabled</key><true/><key>Path</key><string>SSDT-RHUB.aml</string></dict>
</array>
<key>Delete</key><array/>
<key>Patch</key><array/>
<key>Quirks</key> <key>Quirks</key>
<dict> <dict>
<key>FadtEnableReset</key> <false/> <key>FadtEnableReset</key><false/>
<key>NormalizeHeaders</key> <false/> <key>NormalizeHeaders</key><false/>
<key>RebaseRegions</key> <false/> <key>RebaseRegions</key><false/>
<key>ResetHwSig</key> <false/> <key>ResetHwSig</key><false/>
<key>ResetLogoStatus</key> <true/> <key>ResetLogoStatus</key><true/>
<key>SyncTableIds</key> <false/> <key>SyncTableIds</key><false/>
</dict> </dict>
</dict> </dict>
<key>Booter</key> <key>Booter</key>
<dict> <dict>
<key>MmioWhitelist</key> <array/> <key>MmioWhitelist</key><array/>
<key>Patch</key> <array/> <key>Patch</key><array/>
<key>Quirks</key> <key>Quirks</key>
<dict> <dict>
<key>AllowRelocationBlock</key> <false/> <key>AllowRelocationBlock</key><false/>
<key>AvoidRuntimeDefrag</key> <true/> <key>AvoidRuntimeDefrag</key><true/>
<key>DevirtualiseMmio</key> <false/> <!-- Change to true for Alder Lake B660/Z690 if needed --> <key>DevirtualiseMmio</key><true/><!-- Alder Lake: True -->
<key>DisableSingleUser</key> <false/> <key>DisableSingleUser</key><false/>
<key>DisableVariableWrite</key> <false/> <key>DisableVariableWrite</key><false/>
<key>DiscardHibernateMap</key> <false/> <key>DiscardHibernateMap</key><false/>
<key>EnableSafeModeSlide</key> <true/> <key>EnableSafeModeSlide</key><true/>
<key>EnableWriteUnprotector</key> <false/> <!-- Keep false, OpenRuntime handles this --> <key>EnableWriteUnprotector</key><false/>
<key>ForceBooterSignature</key> <false/> <key>ForceBooterSignature</key><false/>
<key>ForceExitBootServices</key> <false/> <key>ForceExitBootServices</key><false/>
<key>ProtectMemoryRegions</key> <false/> <key>ProtectMemoryRegions</key><false/>
<key>ProtectSecureBoot</key> <false/> <key>ProtectSecureBoot</key><false/>
<key>ProtectUefiServices</key> <false/> <key>ProtectUefiServices</key><true/><!-- Alder Lake: True -->
<key>ProvideCustomSlide</key> <true/> <key>ProvideCustomSlide</key><true/>
<key>ProvideMaxSlide</key> <integer>0</integer> <key>ProvideMaxSlide</key><integer>0</integer>
<key>RebuildAppleMemoryMap</key> <false/> <!-- Change to true for Alder Lake if needed --> <key>RebuildAppleMemoryMap</key><true/><!-- Alder Lake: True -->
<key>ResizeAppleGpuBars</key> <integer>-1</integer> <key>ResizeAppleGpuBars</key><integer>-1</integer>
<key>SetupVirtualMap</key> <true/> <key>SetupVirtualMap</key><true/>
<key>SignalAppleOS</key> <false/> <key>SignalAppleOS</key><false/>
<key>SyncRuntimePermissions</key> <false/> <!-- Change to true for Alder Lake if needed --> <key>SyncRuntimePermissions</key><true/><!-- Alder Lake: True -->
</dict> </dict>
</dict> </dict>
<key>DeviceProperties</key> <dict><key>Add</key><dict/><key>Delete</key><dict/></dict> <key>DeviceProperties</key><dict><key>Add</key><dict/><key>Delete</key><dict/></dict>
<key>Kernel</key> <key>Kernel</key>
<dict> <dict>
<key>Add</key> <array> <key>Add</key>
<!-- Lilu --> <array>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>Lilu.kext</string><key>Comment</key><string>Patch engine</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/Lilu</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>Lilu.kext</string><key>Comment</key><string>Lilu</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/Lilu</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<!-- VirtualSMC --> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>VirtualSMC.kext</string><key>Comment</key><string>VirtualSMC</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/VirtualSMC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>VirtualSMC.kext</string><key>Comment</key><string>SMC emulator</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>
<!-- WhateverGreen --> <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>Video patches</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/WhateverGreen</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>WhateverGreen.kext</string><key>Comment</key><string>WhateverGreen for Graphics</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/WhateverGreen</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<!-- AppleALC --> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>AppleALC.kext</string><key>Comment</key><string>AppleALC for Audio</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/AppleALC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>AppleALC.kext</string><key>Comment</key><string>Audio patches</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>
<!-- Ethernet Kexts (disabled by default, enabled by plist_modifier) -->
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>IntelMausi.kext</string><key>Comment</key><string>Intel Ethernet</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/IntelMausi</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>IntelMausi.kext</string><key>Comment</key><string>Intel Ethernet</string><key>Enabled</key><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>RealtekRTL8111.kext</string><key>Comment</key><string>Realtek RTL8111</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/RealtekRTL8111</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>RealtekRTL8111.kext</string><key>Comment</key><string>Realtek RTL8111</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/RealtekRTL8111</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>LucyRTL8125Ethernet.kext</string><key>Comment</key><string>Realtek RTL8125</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/LucyRTL8125Ethernet</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict> <dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>LucyRTL8125Ethernet.kext</string><key>Comment</key><string>Realtek RTL8125 2.5GbE</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/LucyRTL8125Ethernet</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>NVMeFix.kext</string><key>Comment</key><string>NVMe Fixes</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/NVMeFix</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>CpuTopologyRebuild.kext</string><key>Comment</key><string>Alder Lake E-Core/P-Core fix</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/CpuTopologyRebuild</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>RestrictEvents.kext</string><key>Comment</key><string>Restrict unwanted events</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/RestrictEvents</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
</array> </array>
<key>Block</key> <array/> <key>Emulate</key> <dict/> <key>Force</key> <array/> <key>Patch</key> <array/> <key>Block</key><array/>
<key>Emulate</key><dict><key>Cpuid1Data</key><data></data><key>Cpuid1Mask</key><data></data><key>DummyPowerManagement</key><false/><key>MaxKernel</key><string></string><key>MinKernel</key><string></string></dict>
<key>Force</key><array/>
<key>Patch</key><array/>
<key>Quirks</key> <key>Quirks</key>
<dict> <dict>
<key>AppleCpuPmCfgLock</key> <false/> <key>AppleXcpmCfgLock</key> <true/> <key>AppleXcpmExtraMsrs</key> <false/> <key>AppleCpuPmCfgLock</key><false/>
<key>AppleXcpmForceBoost</key> <false/> <key>CustomPciSerialDevice</key> <false/> <key>CustomSMBIOSGuid</key> <false/> <key>AppleXcpmCfgLock</key><true/><!-- Often true for modern Intel -->
<key>DisableIoMapper</key> <true/> <key>DisableLinkeditJettison</key> <true/> <key>DisableRtcChecksum</key> <false/> <key>AppleXcpmExtraMsrs</key><false/>
<key>ExtendBTFeatureFlags</key> <false/> <key>ExternalDiskIcons</key> <false/> <key>ForceAquantiaEthernet</key> <false/> <key>AppleXcpmForceBoost</key><false/>
<key>ForceSecureBootScheme</key> <false/> <key>IncreasePciBarSize</key> <false/> <key>LapicKernelPanic</key> <false/> <key>CustomPciSerialDevice</key><false/>
<key>LegacyCommpage</key> <false/> <key>PanicNoKextDump</key> <true/> <key>PowerTimeoutKernelPanic</key> <true/> <key>CustomSMBIOSGuid</key><false/>
<key>ProvideCurrentCpuInfo</key> <false/> <key>SetApfsTrimTimeout</key> <integer>-1</integer> <key>DisableIoMapper</key><true/>
<key>ThirdPartyDrives</key> <false/> <key>XhciPortLimit</key> <false/> <key>DisableLinkeditJettison</key><true/>
<key>DisableRtcChecksum</key><false/>
<key>ExtendBTFeatureFlags</key><false/>
<key>ExternalDiskIcons</key><false/>
<key>ForceAquantiaEthernet</key><false/>
<key>ForceSecureBootScheme</key><false/>
<key>IncreasePciBarSize</key><false/>
<key>LapicKernelPanic</key><false/>
<key>LegacyCommpage</key><false/>
<key>PanicNoKextDump</key><true/>
<key>PowerTimeoutKernelPanic</key><true/>
<key>ProvideCurrentCpuInfo</key><true/><!-- Alder Lake: True -->
<key>SetApfsTrimTimeout</key><integer>-1</integer>
<key>ThirdPartyDrives</key><false/>
<key>XhciPortLimit</key><false/> <!-- USB Map should handle this -->
</dict> </dict>
<key>Scheme</key> <dict><key>CustomKernel</key><false/><key>FuzzyMatch</key><true/><key>KernelArch</key><string>Auto</string><key>KernelCache</key><string>Auto</string></dict> <key>Scheme</key><dict><key>CustomKernel</key><false/><key>FuzzyMatch</key><true/><key>KernelArch</key><string>Auto</string><key>KernelCache</key><string>Auto</string></dict>
</dict> </dict>
<key>Misc</key> <dict> <key>BlessOverride</key><array/><key>Boot</key><dict><key>ConsoleAttributes</key><integer>0</integer><key>HibernateMode</key><string>None</string><key>HibernateSkipsPicker</key><false/><key>HideAuxiliary</key><false/><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>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>3</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>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</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></dict></dict><key>LegacySchema</key><dict/><key>WriteFlash</key><true/></dict> <key>NVRAM</key><dict><key>Add</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><dict><key>DefaultBackgroundColor</key><data>AAAAAA==</data><key>UIScale</key><data>AQ==</data></dict><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><dict><key>SystemAudioVolume</key><data>Rg==</data><key>boot-args</key><string>-v keepsyms=1 debug=0x100 alcid=1</string><key>csr-active-config</key><data>AAAAAA==</data><key>prev-lang:kbd</key><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>PLEASE_REPLACE_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>PLEASE_REPLACE_SERIAL</string><key>SystemUUID</key><string>PLEASE_REPLACE_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>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></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><false/><key>ReloadOptionRoms</key><false/><key>RequestBootVarRouting</key><true/><key>ResizeGpuBars</key><integer>-1</integer><key>TscSyncTimeout</key><integer>0</integer><key>UnblockFsConnect</key><false/></dict><key>ReservedMemory</key><array/></dict> <key>UEFI</key><dict><key>APFS</key><dict><key>EnableJumpstart</key><true/><key>GlobalConnect</key><false/><key>HideVerbose</key><true/><key>JumpstartHotPlug</key><false/><key>MinDate</key><integer>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>
</dict> </dict>
</plist> </plist>

188
README.md
View File

@ -1,130 +1,130 @@
# Skyscope macOS on PC USB Creator Tool # Skyscope macOS on PC USB Creator Tool
**Version:** 0.8.1 (Alpha) **Version:** 1.0.0 (Dev - New Workflow)
**Developer:** Miss Casey Jay Topojani **Developer:** Miss Casey Jay Topojani
**Business:** Skyscope Sentinel Intelligence **Business:** Skyscope Sentinel Intelligence
## Vision: Your Effortless Bridge to macOS on PC ## Vision: Your Effortless Bridge to macOS on PC
Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that fully automates the complex process of creating a bootable macOS USB drive for virtually any PC. This tool leverages the power of Docker-OSX and OpenCore, aiming to simplify the Hackintosh journey from start to finish. 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.
This project is dedicated to creating a seamless experience, from selecting your desired macOS version to generating a USB drive that's ready to boot your PC into macOS, complete with efforts to auto-configure for your hardware. 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.
## Current Features & Capabilities ## Core Features
* **Intuitive Graphical User Interface (PyQt6):** Guides you through each step of the process. * **Intuitive Graphical User Interface (PyQt6):**
* **macOS Version Selection:** Easily choose from popular macOS versions (Sonoma, Ventura, Monterey, Big Sur, Catalina). * Dark-themed by default (planned).
* **Automated Docker-OSX Orchestration:** * Rounded window design (platform permitting).
* **Intelligent Image Pulling:** Automatically pulls the required `sickcodes/docker-osx` image from Docker Hub, with progress displayed. * Clear, step-by-step workflow.
* **VM Creation & macOS Installation:** Launches the Docker-OSX container where you can interactively install macOS within a QEMU virtual machine. * Enhanced progress indicators (filling bars, spinners, percentage updates - planned).
* **Log Streaming:** View Docker and QEMU logs directly in the application for transparency. * **Automated macOS Installer Acquisition:**
* **VM Image Extraction:** Once macOS is installed in the VM, the tool helps you extract the essential disk images (`mac_hdd_ng.img` and `OpenCore.qcow2`). * Directly downloads official macOS installer assets from Apple's servers using `gibMacOS` principles.
* **Container Management:** Stop and remove the Docker-OSX container after use. * Supports user selection of macOS versions (aiming for Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.).
* **Cross-Platform USB Drive Preparation:** * **Automated USB Installer Creation:**
* **USB Detection:** Identifies potential USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows). * **Cross-Platform USB Detection:** Identifies suitable USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows).
* **Automated EFI & macOS System Write (Linux & macOS):** * **Automated Partitioning:** Creates GUID Partition Table (GPT), an EFI System Partition (FAT32, ~300-550MB), and a main macOS Installer partition (HFS+).
* Partitions the USB drive with a GUID Partition Table (GPT). * **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.
* Creates and formats an EFI System Partition (FAT32) and a main macOS partition (HFS+). * **Intelligent OpenCore EFI Setup:**
* Uses a robust file-level copy (`rsync`) for both EFI content and the main macOS system, ensuring compatibility with various USB sizes and only copying necessary data. * Assembles a complete OpenCore EFI folder on the USB's EFI partition.
* **Windows USB Writing (Partial Automation):** * Includes essential drivers, kexts, and ACPI SSDTs for broad compatibility.
* Automates EFI partition creation and EFI file copying. * **Experimental `config.plist` Auto-Enhancement:**
* **Important:** Writing the main macOS system image currently requires a guided manual step using an external "dd for Windows" utility due to Windows' limitations with direct, scriptable raw partition writing of HFS+/APFS filesystems. The tool prepares the raw image and provides instructions. * If enabled by the user (and running the tool on a Linux host for hardware detection):
* **Experimental `config.plist` Auto-Enhancement:** * Gathers host hardware information (iGPU, dGPU, Audio, Ethernet, CPU).
* **Linux Host Detection:** If the tool is run on a Linux system, it can gather information about your host computer's hardware (iGPU, 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).
* **Targeted Modifications:** Optionally attempts to modify the `config.plist` (from the generated `OpenCore.qcow2`) to: * 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).
* Add common `DeviceProperties` for Intel iGPUs. * Creates a backup of the original `config.plist` before modification.
* Set appropriate audio `layout-id`s. * **Privilege Handling:** Checks for and advises on necessary admin/root privileges for USB writing.
* Ensure necessary Ethernet kexts are enabled. * **User Guidance:** Provides clear instructions and warnings throughout the process.
* Apply boot-args for NVIDIA GTX 970 based on target macOS version (e.g., `nv_disable=1` or `nvda_drv=1`).
* A backup of the original `config.plist` is created before modifications. ## NVIDIA GPU Support Strategy (e.g., GTX 970 on newer macOS)
* **Privilege Checking:** Warns if administrative/root privileges are needed for USB writing and are not detected.
* **UI Feedback:** Status bar messages and an indeterminate progress bar keep you informed during long operations. * **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.
## Current Status & Known Limitations ## Current Status & Known Limitations
* **Windows Main OS USB Write:** This is the primary limitation, requiring a manual `dd` step. Future work aims to automate this if a reliable, redistributable CLI tool for raw partition writing is identified or developed. * **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.
* **`config.plist` Enhancement is Experimental:** * **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.
* Hardware detection for this feature is **currently only implemented for Linux hosts.** On macOS/Windows, the plist modification step will run but won't apply hardware-specific changes. * **`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.
* The applied patches are based on common configurations and may not be optimal or work for all hardware. Always test thoroughly. * **Universal Compatibility:** Hackintoshing is inherently hardware-dependent. While this tool aims for broad compatibility, success on every PC configuration cannot be guaranteed.
* **NVIDIA dGPU Support on Newer macOS:** Modern macOS (Mojave+) does not support NVIDIA Maxwell/Pascal/Turing/Ampere GPUs. The tool attempts to configure systems with these cards for basic display or to use an iGPU if available. Full acceleration is not possible on these macOS versions with these cards. * **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.
* **Universal Compatibility:** While the goal is broad PC compatibility, Hackintoshing can be hardware-specific. Success is not guaranteed on all possible PC configurations.
* **Dependency on External Projects:** Relies on Docker-OSX, OpenCore, and various community-sourced kexts and configurations.
## Prerequisites ## Prerequisites
1. **Docker:** Must be installed and running. Your user account needs permission to manage Docker. 1. **Python:** Version 3.8 or newer.
* [Install Docker Engine](https://docs.docker.com/engine/install/) 2. **Python Libraries:** `PyQt6`, `psutil`. Install via `pip install PyQt6 psutil`.
2. **Python:** Version 3.8 or newer. 3. **Core Utilities (all platforms, must be in PATH):**
3. **Python Libraries:** Install with `pip install PyQt6 psutil`. * `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:** 4. **Platform-Specific CLI Tools for USB Writing:**
* **Linux (e.g., Debian 13 "Trixie"):**
* **Linux (including Debian 13 "Trixie"):** * `sgdisk`, `parted`, `partprobe` (from `gdisk`, `parted`, `util-linux`)
* `qemu-img` (from `qemu-utils`)
* `parted`
* `kpartx` (from `kpartx` or `multipath-tools`)
* `rsync`
* `mkfs.vfat` (from `dosfstools`) * `mkfs.vfat` (from `dosfstools`)
* `mkfs.hfsplus` (from `hfsprogs`) * `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. * `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.
* `lsblk`, `partprobe` (from `util-linux`) * Install most via: `sudo apt update && sudo apt install gdisk parted dosfstools hfsprogs rsync util-linux p7zip-full` (or `p7zip`)
* Install most via: `sudo apt update && sudo apt install qemu-utils parted kpartx rsync dosfstools hfsprogs util-linux`
* **macOS:** * **macOS:**
* `qemu-img` (e.g., via Homebrew: `brew install qemu`) * `diskutil`, `hdiutil`, `rsync`, `cp`, `bless` (standard system tools).
* `diskutil`, `hdiutil`, `rsync` (standard macOS tools). * `7z` (e.g., via Homebrew: `brew install p7zip`).
* **Windows:** * **Windows:**
* `qemu-img` (install and add to PATH). * `diskpart`, `robocopy` (standard system tools).
* `diskpart`, `robocopy` (standard Windows tools). * `7z.exe` (install and add to PATH).
* `7z.exe` (7-Zip command-line tool, install and add to PATH) - for EFI file extraction. * A "dd for Windows" utility (user must install and ensure it's in PATH).
* A "dd for Windows" utility (e.g., from SUSE, chrysocome.net, or similar). Ensure it's in your PATH and you know how to use it for writing to a physical disk's partition or offset.
## How to Run ## How to Run (Development Phase)
1. Ensure all prerequisites for your operating system are met. 1. Ensure all prerequisites for your OS are met.
2. Clone this repository or download the source files. 2. Clone this repository.
3. Install Python libraries: `pip install PyQt6 psutil`. 3. **Crucial:** Clone `corpnewt/gibMacOS` into a `./scripts/gibMacOS/` subdirectory within this project, or ensure `gibMacOS.py` is in the project root or your system PATH and update `GIBMACOS_SCRIPT_PATH` in `main_app.py` if necessary.
4. Execute `python main_app.py`. 4. Install Python libraries: `pip install PyQt6 psutil`.
5. **Important for USB Writing:** 5. Execute `python main_app.py`.
6. **For USB Writing Operations:**
* **Linux:** Run with `sudo python main_app.py`. * **Linux:** Run with `sudo python main_app.py`.
* **macOS:** The script will use `sudo` internally for `rsync` to USB EFI if needed. You might be prompted for your password. Ensure the main application has Full Disk Access if issues arise with `hdiutil` or `diskutil` not having permissions (System Settings > Privacy & Security). * **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.
* **Windows:** Run the application as Administrator. * **Windows:** Run as Administrator.
## Step-by-Step Usage Guide ## Step-by-Step Usage Guide (New Workflow)
1. **Step 1: Create and Install macOS VM** 1. **Step 1: Download macOS Installer Assets**
* Launch the "Skyscope macOS on PC USB Creator Tool". * Launch the "Skyscope macOS on PC USB Creator Tool".
* Select your desired macOS version from the dropdown menu. * Select your desired macOS version (e.g., Sequoia, Sonoma).
* Click "Create VM and Start macOS Installation". * Choose a directory on your computer to save the downloaded assets.
* The tool will first pull the necessary Docker image (progress shown). * Click "Download macOS Installer Assets". The tool will use `gibMacOS` to fetch the official installer files from Apple. This may take time. Progress will be shown.
* Then, a QEMU window will appear. This is your virtual machine. Follow the standard macOS installation procedure within this window (use Disk Utility to erase and format the virtual hard drive, then install macOS). This part is interactive. 2. **Step 2: Create Bootable USB Installer**
* Once macOS is fully installed in QEMU, shut down the macOS VM from within its own interface (Apple Menu > Shut Down). Closing the QEMU window will also terminate the process. * Once downloads are complete, connect your target USB flash drive (16GB+ recommended).
2. **Step 2: Extract VM Images** * Click "Refresh List" to detect USB drives.
* After the Docker process from Step 1 finishes (QEMU window closes), the "Extract Images from Container" button will become active.
* Click it. You'll be prompted to select a directory on your computer. The `mac_hdd_ng.img` (macOS system) and `OpenCore.qcow2` (EFI bootloader) files will be copied here. This may take some time.
3. **Step 3: Container Management (Optional)**
* Once images are extracted, the Docker container used for installation is no longer strictly needed.
* You can "Stop Container" (if it's listed as running by Docker for any reason) and then "Remove Container" to free up disk space.
4. **Step 4: Select Target USB Drive and Write**
* Physically connect your USB flash drive.
* Click "Refresh List".
* **Linux/macOS:** Select your USB drive from the dropdown. Verify size and identifier carefully. * **Linux/macOS:** Select your USB drive from the dropdown. Verify size and identifier carefully.
* **Windows:** USB drives detected via WMI will appear in the dropdown. Select the correct one. Ensure it's the `Disk X` number you intend. * **Windows:** USB drives detected via WMI will appear in the dropdown. Select the correct one. Ensure it's the `Disk X` number you intend.
* **(Optional, Experimental):** Check the "Try to auto-enhance config.plist..." box if you are on a Linux host and wish to attempt automatic `config.plist` modification for your hardware. A backup of the original `config.plist` will be made. * **(Optional, Experimental):** Check the "Try to auto-enhance config.plist..." box if you are on a Linux host and wish the tool to attempt automatic `config.plist` modification for your hardware. A backup of the original `config.plist` will be made.
* **CRITICAL WARNING:** Double-check your selection. The next action will erase the selected USB drive. * **CRITICAL WARNING:** Double-check your USB selection. The next action will erase the entire USB drive.
* Click "Write Images to USB Drive". Confirm the data erasure warning. * Click "Create macOS Installer USB". Confirm the data erasure warning.
* The process will now: * The tool will:
* (If enhancement enabled) Attempt to modify the `config.plist` within the source OpenCore image. * Partition and format the USB drive.
* Partition and format your USB drive. * Extract and write the macOS BaseSystem to make the USB bootable.
* Copy EFI files to the USB's EFI partition. * Copy necessary macOS installer packages and files to the USB.
* Copy macOS system files to the USB's main partition. (On Windows, this step requires manual `dd` operation as guided by the application). * Assemble an OpenCore EFI folder (potentially with your hardware-specific enhancements if enabled) onto the USB's EFI partition.
* This is a lengthy process. Monitor the progress in the output area. * This is a lengthy process. Monitor progress in the output area and status bar.
5. **Boot!** 3. **Boot Your PC from the USB!**
* Once complete, safely eject the USB drive. You can now try booting your PC from it. Remember to configure your PC's BIOS/UEFI for booting from USB and for macOS compatibility (e.g., disable Secure Boot, enable AHCI, XHCI Handoff, etc., as per standard Hackintosh guides like Dortania). * Safely eject the USB. Configure your PC's BIOS/UEFI for macOS booting (disable Secure Boot, enable AHCI, XHCI Handoff, etc. - see Dortania guides).
* Boot from the USB and proceed with macOS installation onto your PC's internal drive.
4. **(For Unsupported NVIDIA on newer macOS): Post-Install Patching**
* After installing macOS, if you have an unsupported NVIDIA card (like GTX 970 on Sonoma/Sequoia) and want graphics acceleration, you will need to run the **OpenCore Legacy Patcher (OCLP)** application from within your new macOS installation. This tool has prepared the EFI to be generally compatible with OCLP.
## Future Vision & Enhancements ## Future Vision & Advanced Capabilities
* **Fully Automated Windows USB Writing:** Replace the manual `dd` step with a reliable, integrated solution. * **Fully Automated Windows USB Writing:** Replace the manual `dd` step with a reliable, integrated solution.
* **Advanced `config.plist` Customization:** * **Advanced `config.plist` Customization:**
* Expand hardware detection to macOS and Windows hosts. * Expand hardware detection for plist enhancement to macOS and Windows hosts.
* Provide more granular UI controls for plist enhancements (e.g., preview changes, select specific patches). * Provide more granular UI controls for plist enhancements (e.g., preview changes, select specific patches).
* Allow users to load/save `config.plist` modification profiles. * Allow users to load/save `config.plist` modification profiles.
* **Enhanced UI/UX for Progress:** Implement determinate progress bars with percentage completion and more dynamic status updates. * **Enhanced UI/UX for Progress:** Implement determinate progress bars with percentage completion and more dynamic status updates.
@ -133,7 +133,7 @@ This project is dedicated to creating a seamless experience, from selecting your
## Contributing ## Contributing
Your contributions, feedback, and bug reports are highly welcome! Please fork the repository and submit pull requests, or open issues for discussion. We are passionate about making Hackintoshing more accessible! Contributions, feedback, and bug reports are highly encouraged.
## License ## License

View File

@ -6,8 +6,8 @@ import psutil
import platform import platform
import ctypes import ctypes
import json import json
import re import re # For progress parsing
import traceback # For better error logging import traceback # For error reporting
import shutil # For shutil.which import shutil # For shutil.which
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
@ -16,7 +16,7 @@ from PyQt6.QtWidgets import (
QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox
) )
from PyQt6.QtGui import QAction, QIcon from PyQt6.QtGui import QAction, QIcon
from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, QTimer, Qt from PyQt6.QtCore import pyqtSignal, pyqtSlot, QObject, QThread, QTimer, Qt # Added QTimer
from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS from constants import APP_NAME, DEVELOPER_NAME, BUSINESS_NAME, MACOS_VERSIONS
# DOCKER_IMAGE_BASE and Docker-related utils are no longer primary for this flow. # DOCKER_IMAGE_BASE and Docker-related utils are no longer primary for this flow.
@ -37,16 +37,21 @@ elif platform.system() == "Windows":
try: from usb_writer_windows import USBWriterWindows try: from usb_writer_windows import USBWriterWindows
except ImportError as e: print(f"Could not import USBWriterWindows: {e}") except ImportError as e: print(f"Could not import USBWriterWindows: {e}")
# Path to gibMacOS.py script. Assumed to be in a 'scripts' subdirectory.
# The application startup or a setup step should ensure gibMacOS is cloned/present here.
GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "scripts", "gibMacOS", "gibMacOS.py") GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "scripts", "gibMacOS", "gibMacOS.py")
if not os.path.exists(GIBMACOS_SCRIPT_PATH): if not os.path.exists(GIBMACOS_SCRIPT_PATH):
# Fallback if not in relative scripts dir, try to find it in current dir (e.g. if user placed it there)
GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "gibMacOS.py") GIBMACOS_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "gibMacOS.py")
class WorkerSignals(QObject): class WorkerSignals(QObject):
progress = pyqtSignal(str) progress = pyqtSignal(str)
finished = pyqtSignal(str) finished = pyqtSignal(str) # Can carry a success message or final status
error = pyqtSignal(str) error = pyqtSignal(str)
progress_value = pyqtSignal(int) # New signal for determinate progress
progress_value = pyqtSignal(int) # Percentage 0-100
class GibMacOSWorker(QObject): class GibMacOSWorker(QObject):
signals = WorkerSignals() signals = WorkerSignals()
@ -61,16 +66,15 @@ class GibMacOSWorker(QObject):
@pyqtSlot() @pyqtSlot()
def run(self): def run(self):
try: try:
script_to_run = "" script_to_run = GIBMACOS_SCRIPT_PATH
if os.path.exists(GIBMACOS_SCRIPT_PATH): if not os.path.exists(script_to_run):
script_to_run = GIBMACOS_SCRIPT_PATH alt_script_path = os.path.join(os.path.dirname(os.path.dirname(GIBMACOS_SCRIPT_PATH)), "gibMacOS.py") # if main_app is in src/
elif shutil.which("gibMacOS.py"): # Check if it's in PATH script_to_run = alt_script_path if os.path.exists(alt_script_path) else "gibMacOS.py"
script_to_run = "gibMacOS.py" if not os.path.exists(script_to_run) and not shutil.which(script_to_run): # Check if it's in PATH
elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")): # Check alongside main_app.py self.signals.error.emit(f"gibMacOS.py not found at expected locations ({GIBMACOS_SCRIPT_PATH}, {alt_script_path}) or in PATH.")
script_to_run = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py") return
else: else:
self.signals.error.emit(f"gibMacOS.py not found at expected locations or in PATH.") script_to_run = GIBMACOS_SCRIPT_PATH
return
version_for_gib = MACOS_VERSIONS.get(self.version_key, self.version_key) version_for_gib = MACOS_VERSIONS.get(self.version_key, self.version_key)
os.makedirs(self.download_path, exist_ok=True) os.makedirs(self.download_path, exist_ok=True)
@ -91,10 +95,15 @@ class GibMacOSWorker(QObject):
break break
line_strip = line.strip() line_strip = line.strip()
self.signals.progress.emit(line_strip) self.signals.progress.emit(line_strip)
progress_match = re.search(r"(\d+)%", line_strip) progress_match = re.search(r"\(?\s*(\d{1,3}\.?\d*)\s*%\s*\)?", line_strip)
if progress_match: if progress_match:
try: self.signals.progress_value.emit(int(progress_match.group(1))) try:
except ValueError: pass percent = int(float(progress_match.group(1)))
self.signals.progress_value.emit(percent)
except ValueError:
pass # Ignore if not a valid int
elif "downloaded 100.00%" in line_strip.lower():
self.signals.progress_value.emit(100)
self.process.stdout.close() self.process.stdout.close()
return_code = self.process.wait() return_code = self.process.wait()
@ -108,7 +117,7 @@ class GibMacOSWorker(QObject):
else: else:
self.signals.error.emit(f"Failed to download macOS '{self.version_key}' (gibMacOS exit code {return_code}). Check logs.") self.signals.error.emit(f"Failed to download macOS '{self.version_key}' (gibMacOS exit code {return_code}). Check logs.")
except FileNotFoundError: except FileNotFoundError:
self.signals.error.emit(f"Error: Python or gibMacOS.py script not found. Ensure Python is in PATH and gibMacOS script is correctly located.") self.signals.error.emit(f"Error: Python or gibMacOS.py script not found. Ensure Python is in PATH and gibMacOS script is correctly located (tried: {GIBMACOS_SCRIPT_PATH}).")
except Exception as e: except Exception as e:
self.signals.error.emit(f"An error occurred during macOS download: {str(e)}\n{traceback.format_exc()}") self.signals.error.emit(f"An error occurred during macOS download: {str(e)}\n{traceback.format_exc()}")
finally: finally:
@ -147,9 +156,6 @@ class USBWriterWorker(QObject):
if writer_cls is None: if writer_cls is None:
self.signals.error.emit(f"{current_os} USB writer module not available or OS not supported."); return self.signals.error.emit(f"{current_os} USB writer module not available or OS not supported."); return
# Platform writers' __init__ will need to be updated for macos_download_path
# This assumes usb_writer_*.py __init__ signatures are now:
# __init__(self, device, macos_download_path, progress_callback, enhance_plist_enabled, target_macos_version)
self.writer_instance = writer_cls( self.writer_instance = writer_cls(
device=self.device, device=self.device,
macos_download_path=self.macos_download_path, macos_download_path=self.macos_download_path,
@ -158,6 +164,11 @@ class USBWriterWorker(QObject):
target_macos_version=self.target_macos_version target_macos_version=self.target_macos_version
) )
# Check if writer_instance has 'signals' attribute for progress_value (for rsync progress later)
# This is more for future-proofing if USB writers implement determinate progress.
if hasattr(self.writer_instance, 'signals') and hasattr(self.writer_instance.signals, 'progress_value'):
self.writer_instance.signals.progress_value.connect(self.signals.progress_value.emit)
if self.writer_instance.format_and_write(): if self.writer_instance.format_and_write():
self.signals.finished.emit("USB writing process completed successfully.") self.signals.finished.emit("USB writing process completed successfully.")
else: else:
@ -170,7 +181,7 @@ class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle(APP_NAME) self.setWindowTitle(APP_NAME)
self.setGeometry(100, 100, 800, 700) # Adjusted height self.setGeometry(100, 100, 800, 750)
self.active_worker_thread = None self.active_worker_thread = None
self.macos_download_path = None self.macos_download_path = None
@ -182,7 +193,7 @@ class MainWindow(QMainWindow):
self._setup_ui() self._setup_ui()
self.status_bar = self.statusBar() self.status_bar = self.statusBar()
# self.status_bar.addPermanentWidget(self.progress_bar) # Progress bar now in main layout # self.status_bar.addPermanentWidget(self.progress_bar) # Progress bar now added to main layout
self.status_bar.showMessage(self.base_status_message, 5000) self.status_bar.showMessage(self.base_status_message, 5000)
self.refresh_usb_drives() self.refresh_usb_drives()
@ -192,25 +203,15 @@ class MainWindow(QMainWindow):
about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action) about_action = QAction("&About", self); about_action.triggered.connect(self.show_about_dialog); help_menu.addAction(about_action)
central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget) central_widget = QWidget(); self.setCentralWidget(central_widget); main_layout = QVBoxLayout(central_widget)
# Step 1: Download macOS download_group = QGroupBox("Step 1: Download macOS Installer Assets"); download_layout = QVBoxLayout()
download_group = QGroupBox("Step 1: Download macOS Installer Assets")
download_layout = QVBoxLayout()
selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox() selection_layout = QHBoxLayout(); self.version_label = QLabel("Select macOS Version:"); self.version_combo = QComboBox()
self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo) self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo)
download_layout.addLayout(selection_layout) download_layout.addLayout(selection_layout); self.download_macos_button = QPushButton("Download macOS Installer Assets")
self.download_macos_button.clicked.connect(self.start_macos_download_flow); download_layout.addWidget(self.download_macos_button)
self.download_macos_button = QPushButton("Download macOS Installer Assets")
self.download_macos_button.clicked.connect(self.start_macos_download_flow)
download_layout.addWidget(self.download_macos_button)
self.cancel_operation_button = QPushButton("Cancel Current Operation") self.cancel_operation_button = QPushButton("Cancel Current Operation")
self.cancel_operation_button.clicked.connect(self.stop_current_operation) self.cancel_operation_button.clicked.connect(self.stop_current_operation)
self.cancel_operation_button.setEnabled(False) self.cancel_operation_button.setEnabled(False); download_layout.addWidget(self.cancel_operation_button); download_group.setLayout(download_layout); main_layout.addWidget(download_group)
download_layout.addWidget(self.cancel_operation_button)
download_group.setLayout(download_layout)
main_layout.addWidget(download_group)
# Step 2: USB Drive Selection & Writing
usb_group = QGroupBox("Step 2: Create Bootable USB Installer") usb_group = QGroupBox("Step 2: Create Bootable USB Installer")
self.usb_layout = QVBoxLayout() self.usb_layout = QVBoxLayout()
self.usb_drive_label = QLabel("Available USB Drives:"); self.usb_layout.addWidget(self.usb_drive_label) self.usb_drive_label = QLabel("Available USB Drives:"); self.usb_layout.addWidget(self.usb_drive_label)
@ -229,35 +230,56 @@ class MainWindow(QMainWindow):
self.progress_bar = QProgressBar(self); self.progress_bar.setRange(0, 0); self.progress_bar.setVisible(False); main_layout.addWidget(self.progress_bar) self.progress_bar = QProgressBar(self); self.progress_bar.setRange(0, 0); self.progress_bar.setVisible(False); main_layout.addWidget(self.progress_bar)
self.output_area = QTextEdit(); self.output_area.setReadOnly(True); main_layout.addWidget(self.output_area) self.output_area = QTextEdit(); self.output_area.setReadOnly(True); main_layout.addWidget(self.output_area)
# self.statusBar.addPermanentWidget(self.progress_bar) # Removed from here, progress bar now in main layout
self.update_all_button_states() self.update_all_button_states()
def show_about_dialog(self): QMessageBox.about(self, f"About {APP_NAME}", f"Version: 1.0.0 (Installer Flow)\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using gibMacOS and OpenCore.")
def show_about_dialog(self): QMessageBox.about(self, f"About {APP_NAME}", f"Version: 1.0.1\nDeveloper: {DEVELOPER_NAME}\nBusiness: {BUSINESS_NAME}\n\nThis tool helps create bootable macOS USB drives using gibMacOS and OpenCore.")
def _set_ui_busy(self, busy_status: bool, message: str = "Processing..."): def _set_ui_busy(self, busy_status: bool, message: str = "Processing..."):
# Disable/Enable general interactive widgets
general_widgets_to_manage = [
self.download_macos_button, self.version_combo,
self.refresh_usb_button, self.usb_drive_combo,
self.windows_disk_id_input, self.enhance_plist_checkbox,
self.write_to_usb_button # Write button is also general now
]
for widget in general_widgets_to_manage:
widget.setEnabled(not busy_status)
# Specific button for ongoing operation
self.cancel_operation_button.setEnabled(busy_status and self.current_worker_instance is not None)
self.progress_bar.setVisible(busy_status) self.progress_bar.setVisible(busy_status)
if busy_status: if busy_status:
self.base_status_message = message self.base_status_message = message
if not self.spinner_timer.isActive(): self.spinner_timer.start(150) if not self.spinner_timer.isActive(): self.spinner_timer.start(150)
self._update_spinner_status() self._update_spinner_status()
self.progress_bar.setRange(0,0) # Progress bar range set by _start_worker based on provides_progress
else: else:
self.spinner_timer.stop() self.spinner_timer.stop()
self.status_bar.showMessage(message or "Ready.", 7000) self.status_bar.showMessage(message or "Ready.", 7000)
self.update_all_button_states()
if not busy_status: # After an operation, always update all button states
self.update_all_button_states()
def _update_spinner_status(self): def _update_spinner_status(self):
if self.spinner_timer.isActive(): if self.spinner_timer.isActive():
char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)] char = self.spinner_chars[self.spinner_index % len(self.spinner_chars)]
current_message = self.base_status_message
# Check if current worker is providing determinate progress
active_worker_provides_progress = False active_worker_provides_progress = False
if self.active_worker_thread and self.active_worker_thread.isRunning(): if self.active_worker_thread and self.active_worker_thread.isRunning():
active_worker_provides_progress = getattr(self.active_worker_thread, "provides_progress", False) active_worker_provides_progress = getattr(self.active_worker_thread, "provides_progress", False)
if active_worker_provides_progress and self.progress_bar.maximum() == 100: # Determinate if active_worker_provides_progress and self.progress_bar.maximum() == 100: # Determinate
self.status_bar.showMessage(f"{char} {self.base_status_message} ({self.progress_bar.value()}%)") current_message = f"{self.base_status_message} ({self.progress_bar.value()}%)"
else: else: # Indeterminate
if self.progress_bar.maximum() != 0: self.progress_bar.setRange(0,0) if self.progress_bar.maximum() != 0: self.progress_bar.setRange(0,0)
self.status_bar.showMessage(f"{char} {self.base_status_message}")
self.status_bar.showMessage(f"{char} {current_message}")
self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars) self.spinner_index = (self.spinner_index + 1) % len(self.spinner_chars)
elif not (self.active_worker_thread and self.active_worker_thread.isRunning()): elif not (self.active_worker_thread and self.active_worker_thread.isRunning()):
self.spinner_timer.stop() self.spinner_timer.stop()
@ -301,14 +323,23 @@ class MainWindow(QMainWindow):
self.current_worker_instance = worker_instance self.current_worker_instance = worker_instance
if provides_progress: if provides_progress:
self.progress_bar.setRange(0,100) self.progress_bar.setRange(0,100); self.progress_bar.setValue(0)
worker_instance.signals.progress_value.connect(self.update_progress_bar_value) # Ensure signal exists on worker before connecting
if hasattr(worker_instance.signals, 'progress_value'):
worker_instance.signals.progress_value.connect(self.update_progress_bar_value)
else:
self._report_progress(f"Warning: Worker '{worker_name}' set to provides_progress=True but has no 'progress_value' signal.")
self.progress_bar.setRange(0,0) # Fallback to indeterminate
provides_progress = False # Correct the flag
else: else:
self.progress_bar.setRange(0,0) self.progress_bar.setRange(0,0)
self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread") self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread")
setattr(self.active_worker_thread, "provides_progress", provides_progress) setattr(self.active_worker_thread, "provides_progress", provides_progress)
# Store specific instance type for stop_current_operation if needed
if worker_name == "macos_download": self.gibmacos_worker_instance = worker_instance
worker_instance.moveToThread(self.active_worker_thread) worker_instance.moveToThread(self.active_worker_thread)
worker_instance.signals.progress.connect(self.update_output) worker_instance.signals.progress.connect(self.update_output)
worker_instance.signals.finished.connect(lambda msg, wn=worker_name, slot=on_finished_slot: self._handle_worker_finished(msg, wn, slot)) worker_instance.signals.finished.connect(lambda msg, wn=worker_name, slot=on_finished_slot: self._handle_worker_finished(msg, wn, slot))
@ -322,18 +353,24 @@ class MainWindow(QMainWindow):
def update_progress_bar_value(self, value): def update_progress_bar_value(self, value):
if self.progress_bar.maximum() == 0: self.progress_bar.setRange(0,100) if self.progress_bar.maximum() == 0: self.progress_bar.setRange(0,100)
self.progress_bar.setValue(value) self.progress_bar.setValue(value)
# Spinner update will happen on its timer, it can check progress_bar.value() # Update base_status_message for spinner to include percentage
if self.active_worker_thread and self.active_worker_thread.isRunning():
worker_name_display = self.active_worker_thread.objectName().replace("_thread","").replace("_"," ").capitalize()
self.base_status_message = f"{worker_name_display} in progress..." # Keep it generic or pass specific msg
# The spinner timer will pick up self.progress_bar.value()
def _handle_worker_finished(self, message, worker_name, specific_finished_slot): def _handle_worker_finished(self, message, worker_name, specific_finished_slot):
final_msg = f"{worker_name.replace('_', ' ').capitalize()} completed." final_msg = f"{worker_name.replace('_', ' ').capitalize()} completed."
self.current_worker_instance = None # Clear current worker if worker_name == "macos_download": self.gibmacos_worker_instance = None # Clear specific instance
self.current_worker_instance = None
self.active_worker_thread = None self.active_worker_thread = None
if specific_finished_slot: specific_finished_slot(message) if specific_finished_slot: specific_finished_slot(message)
self._set_ui_busy(False, final_msg) self._set_ui_busy(False, final_msg)
def _handle_worker_error(self, error_message, worker_name, specific_error_slot): def _handle_worker_error(self, error_message, worker_name, specific_error_slot):
final_msg = f"{worker_name.replace('_', ' ').capitalize()} failed." final_msg = f"{worker_name.replace('_', ' ').capitalize()} failed."
self.current_worker_instance = None # Clear current worker if worker_name == "macos_download": self.gibmacos_worker_instance = None # Clear specific instance
self.current_worker_instance = None
self.active_worker_thread = None self.active_worker_thread = None
if specific_error_slot: specific_error_slot(error_message) if specific_error_slot: specific_error_slot(error_message)
self._set_ui_busy(False, final_msg) self._set_ui_busy(False, final_msg)
@ -346,33 +383,43 @@ class MainWindow(QMainWindow):
if not chosen_path: self.output_area.append("Download directory selection cancelled."); return if not chosen_path: self.output_area.append("Download directory selection cancelled."); return
self.macos_download_path = chosen_path self.macos_download_path = chosen_path
# self.output_area.append(f"Starting macOS {selected_version_name} download to: {self.macos_download_path}...") # Message handled by _set_ui_busy
worker = GibMacOSWorker(gibmacos_version_arg, self.macos_download_path) worker = GibMacOSWorker(gibmacos_version_arg, self.macos_download_path)
if not self._start_worker(worker, self.macos_download_finished, self.macos_download_error, if not self._start_worker(worker, self.macos_download_finished, self.macos_download_error,
"macos_download", "macos_download", # worker_name
f"Downloading macOS {selected_version_name} assets...", f"Downloading macOS {selected_version_name} assets...", # busy_message
provides_progress=True): # Assuming GibMacOSWorker will emit progress_value provides_progress=True): # GibMacOSWorker now attempts to provide progress
self._set_ui_busy(False, "Failed to start macOS download operation.") self._set_ui_busy(False, "Failed to start macOS download operation.")
@pyqtSlot(str) @pyqtSlot(str)
def macos_download_finished(self, message): def macos_download_finished(self, message):
# self.output_area.append(f"macOS Download Finished: {message}") # Logged by generic handler
QMessageBox.information(self, "Download Complete", message) QMessageBox.information(self, "Download Complete", message)
# self.macos_download_path is set. UI update handled by generic handler. # self.macos_download_path is set. UI update handled by generic handler.
@pyqtSlot(str) @pyqtSlot(str)
def macos_download_error(self, error_message): def macos_download_error(self, error_message):
# self.output_area.append(f"macOS Download Error: {error_message}") # Logged by generic handler
QMessageBox.critical(self, "Download Error", error_message) QMessageBox.critical(self, "Download Error", error_message)
self.macos_download_path = None self.macos_download_path = None
# UI reset by generic handler. # UI reset by generic handler.
def stop_current_operation(self): def stop_current_operation(self):
if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'): if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'):
self.output_area.append(f" worker_name_display = "Operation"
--- Attempting to stop {self.active_worker_thread.objectName().replace('_thread','')} ---") if self.active_worker_thread: # Get worker name if possible
worker_name_display = self.active_worker_thread.objectName().replace('_thread','').replace('_',' ').capitalize()
self.output_area.append(f"\n--- Attempting to stop {worker_name_display} ---")
self.current_worker_instance.stop() self.current_worker_instance.stop()
else: else:
self.output_area.append(" self.output_area.append("\n--- No active stoppable operation or stop method not implemented for current worker. ---")
--- No active stoppable operation or stop method not implemented for current worker. ---") # UI state will be updated when the worker actually finishes or errors out due to stop.
# We can disable the cancel button here to prevent multiple clicks if desired,
# but update_all_button_states will also handle it.
self.cancel_operation_button.setEnabled(False)
def handle_error(self, message): def handle_error(self, message):
self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message) self.output_area.append(f"ERROR: {message}"); QMessageBox.critical(self, "Error", message)
@ -385,12 +432,17 @@ class MainWindow(QMainWindow):
except Exception as e: self.output_area.append(f"Could not check admin privileges: {e}"); return False except Exception as e: self.output_area.append(f"Could not check admin privileges: {e}"); return False
def refresh_usb_drives(self): # ... (same logic as before) def refresh_usb_drives(self): # ... (same logic as before)
self.usb_drive_combo.clear(); current_selection_text = getattr(self, '_current_usb_selection_text', None) self.usb_drive_combo.clear(); current_selection_text = getattr(self, '_current_usb_selection_text', None); self.output_area.append("\nScanning for disk devices...")
self.output_area.append(" current_os = platform.system()
Scanning for disk devices...") self.windows_usb_guidance_label.setVisible(current_os == "Windows")
if platform.system() == "Windows": # Show/hide manual input field based on whether WMI found drives or failed
# This logic is now more refined within the Windows block
self.usb_drive_combo.setVisible(True)
if current_os == "Windows":
self.usb_drive_label.setText("Available USB Disks (Windows - via WMI/PowerShell):") self.usb_drive_label.setText("Available USB Disks (Windows - via WMI/PowerShell):")
self.windows_usb_guidance_label.setVisible(True); self.windows_disk_id_input.setVisible(False); self.windows_disk_id_input.setVisible(False) # Hide initially, show on WMI error/no results
self.windows_usb_input_label.setVisible(False)
powershell_command = "Get-WmiObject Win32_DiskDrive | Where-Object {$_.InterfaceType -eq 'USB'} | Select-Object DeviceID, Index, Model, @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}} | ConvertTo-Json" powershell_command = "Get-WmiObject Win32_DiskDrive | Where-Object {$_.InterfaceType -eq 'USB'} | Select-Object DeviceID, Index, Model, @{Name='SizeGB';Expression={[math]::Round($_.Size / 1GB, 2)}} | ConvertTo-Json"
try: try:
process = subprocess.run(["powershell", "-Command", powershell_command], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW) process = subprocess.run(["powershell", "-Command", powershell_command], capture_output=True, text=True, check=True, creationflags=subprocess.CREATE_NO_WINDOW)
@ -404,17 +456,21 @@ Scanning for disk devices...")
if current_selection_text: if current_selection_text:
for i in range(self.usb_drive_combo.count()): for i in range(self.usb_drive_combo.count()):
if self.usb_drive_combo.itemText(i) == current_selection_text: self.usb_drive_combo.setCurrentIndex(i); break if self.usb_drive_combo.itemText(i) == current_selection_text: self.usb_drive_combo.setCurrentIndex(i); break
else: self.output_area.append("No USB disks found via WMI/PowerShell. Manual input field shown as fallback."); self.windows_disk_id_input.setVisible(True) else:
except Exception as e: self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}"); self.windows_disk_id_input.setVisible(True) self.output_area.append("No USB disks found via WMI/PowerShell. Manual Disk Number input enabled below.");
self.windows_usb_input_label.setVisible(True); self.windows_disk_id_input.setVisible(True)
except Exception as e:
self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}. Manual Disk Number input enabled below.")
self.windows_usb_input_label.setVisible(True); self.windows_disk_id_input.setVisible(True)
else: else:
self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):") self.usb_drive_label.setText("Available USB Drives (for Linux/macOS):")
self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False) self.windows_usb_guidance_label.setVisible(False); self.windows_disk_id_input.setVisible(False); self.windows_usb_input_label.setVisible(False)
try: try:
partitions = psutil.disk_partitions(all=False); potential_usbs = [] partitions = psutil.disk_partitions(all=False); potential_usbs = []
for p in partitions: for p in partitions:
is_removable = 'removable' in p.opts; is_likely_usb = False is_removable = 'removable' in p.opts; is_likely_usb = False
if platform.system() == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True if current_os == "Darwin" and p.device.startswith("/dev/disk") and 'external' in p.opts.lower() and 'physical' in p.opts.lower(): is_likely_usb = True
elif platform.system() == "Linux" and ((p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da"))): is_likely_usb = True elif current_os == "Linux" and ((p.mountpoint and ("/media/" in p.mountpoint or "/run/media/" in p.mountpoint)) or (p.device.startswith("/dev/sd") and not p.device.endswith("da"))): is_likely_usb = True
if is_removable or is_likely_usb: if is_removable or is_likely_usb:
try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3) try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3)
except Exception: continue except Exception: continue
@ -437,11 +493,19 @@ Scanning for disk devices...")
if not self.check_admin_privileges(): QMessageBox.warning(self, "Privileges Required", "This operation requires Administrator/root privileges."); return if not self.check_admin_privileges(): QMessageBox.warning(self, "Privileges Required", "This operation requires Administrator/root privileges."); return
if not self.macos_download_path or not os.path.isdir(self.macos_download_path): QMessageBox.warning(self, "Missing macOS Assets", "Download macOS installer assets first."); return if not self.macos_download_path or not os.path.isdir(self.macos_download_path): QMessageBox.warning(self, "Missing macOS Assets", "Download macOS installer assets first."); return
current_os = platform.system(); usb_writer_module = None; target_device_id_for_worker = None current_os = platform.system(); usb_writer_module = None; target_device_id_for_worker = None
if current_os == "Windows": target_device_id_for_worker = self.usb_drive_combo.currentData() or self.windows_disk_id_input.text().strip(); usb_writer_module = USBWriterWindows if current_os == "Windows":
target_device_id_for_worker = self.usb_drive_combo.currentData()
if not target_device_id_for_worker and self.windows_disk_id_input.isVisible(): # Fallback to manual input IF VISIBLE
target_device_id_for_worker = self.windows_disk_id_input.text().strip()
if not target_device_id_for_worker or not target_device_id_for_worker.isdigit(): # Must be a digit (disk index)
QMessageBox.warning(self, "Input Required", "Please select a valid USB disk from dropdown or enter its Disk Number if WMI failed."); return
usb_writer_module = USBWriterWindows
else: target_device_id_for_worker = self.usb_drive_combo.currentData(); usb_writer_module = USBWriterLinux if current_os == "Linux" else USBWriterMacOS if current_os == "Darwin" else None else: target_device_id_for_worker = self.usb_drive_combo.currentData(); usb_writer_module = USBWriterLinux if current_os == "Linux" else USBWriterMacOS if current_os == "Darwin" else None
if not usb_writer_module: QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported for {current_os}."); return if not usb_writer_module: QMessageBox.warning(self, "Unsupported Platform", f"USB writing not supported for {current_os}."); return
if not target_device_id_for_worker: QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify target USB."); return if not target_device_id_for_worker: QMessageBox.warning(self, "No USB Selected/Identified", f"Please select/identify target USB."); return
if current_os == "Windows" and target_device_id_for_worker.isdigit(): target_device_id_for_worker = f"disk {target_device_id_for_worker}" # For Windows, USBWriterWindows expects just the number string.
# For Linux/macOS, it's the device path like /dev/sdx or /dev/diskX.
enhance_plist_state = self.enhance_plist_checkbox.isChecked() enhance_plist_state = self.enhance_plist_checkbox.isChecked()
target_macos_name = self.version_combo.currentText() target_macos_name = self.version_combo.currentText()
@ -450,19 +514,10 @@ Proceed?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel, Q
if reply == QMessageBox.StandardButton.Cancel: self.output_area.append(" if reply == QMessageBox.StandardButton.Cancel: self.output_area.append("
USB write cancelled."); return USB write cancelled."); return
# USBWriterWorker now needs different args usb_worker = USBWriterWorker(device=target_device_id_for_worker, macos_download_path=self.macos_download_path, enhance_plist=enhance_plist_state, target_macos_version=target_macos_name)
# The platform specific writers (USBWriterLinux etc) will need to be updated to accept macos_download_path if not self._start_worker(usb_worker, self.usb_write_finished, self.usb_write_error, "usb_write_worker",
# and use it to find BaseSystem.dmg, EFI/OC etc. instead of opencore_qcow2_path, macos_qcow2_path
usb_worker_adapted = USBWriterWorker(
device=target_device_id_for_worker,
macos_download_path=self.macos_download_path,
enhance_plist=enhance_plist_state,
target_macos_version=target_macos_name
)
if not self._start_worker(usb_worker_adapted, self.usb_write_finished, self.usb_write_error, "usb_write_worker",
busy_message=f"Creating USB for {target_device_id_for_worker}...", busy_message=f"Creating USB for {target_device_id_for_worker}...",
provides_progress=False): # USB writing can be long, but progress parsing is per-platform script. provides_progress=False): # USB write progress is complex, indeterminate for now
self._set_ui_busy(False, "Failed to start USB write operation.") self._set_ui_busy(False, "Failed to start USB write operation.")
@pyqtSlot(str) @pyqtSlot(str)
@ -470,7 +525,7 @@ USB write cancelled."); return
@pyqtSlot(str) @pyqtSlot(str)
def usb_write_error(self, error_message): QMessageBox.critical(self, "USB Write Error", error_message) def usb_write_error(self, error_message): QMessageBox.critical(self, "USB Write Error", error_message)
def closeEvent(self, event): # ... (same logic) def closeEvent(self, event):
self._current_usb_selection_text = self.usb_drive_combo.currentText() self._current_usb_selection_text = self.usb_drive_combo.currentText()
if self.active_worker_thread and self.active_worker_thread.isRunning(): if self.active_worker_thread and self.active_worker_thread.isRunning():
reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) reply = QMessageBox.question(self, 'Confirm Exit', "An operation is running. Exit anyway?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
@ -483,9 +538,5 @@ USB write cancelled."); return
if __name__ == "__main__": if __name__ == "__main__":
import traceback # Ensure traceback is available for GibMacOSWorker import traceback; import shutil
import shutil # Ensure shutil is available for GibMacOSWorker path check app = QApplication(sys.argv); window = MainWindow(); window.show(); sys.exit(app.exec())
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())

View File

@ -20,33 +20,33 @@ else:
get_audio_codecs = lambda: [] # Dummy function for non-Linux get_audio_codecs = lambda: [] # Dummy function for non-Linux
# --- Mappings --- # --- Mappings ---
# Values are typically byte-swapped for device-id and some ig-platform-id representations in OpenCore # For AAPL,ig-platform-id, byte order in <Data> can be direct or swapped depending on source.
# For AAPL,ig-platform-id, the first two bytes are often the device-id (swapped), last two are platform related. # OpenCore usually expects direct byte order for data values (e.g. 0A009B46 for 0x469B000A).
# Example: UHD 630 (Desktop Coffee Lake) device-id 0x3E9B -> data <9B3E0000> # The values below are what should be written as data (hex bytes).
# ig-platform-id commonly 0x3E9B0007 -> data <07009B3E> (or other variants)
INTEL_IGPU_DEFAULTS = { INTEL_IGPU_DEFAULTS = {
# Coffee Lake Desktop (UHD 630) - Common # Coffee Lake Desktop (UHD 630)
"8086:3e9b": {"AAPL,ig-platform-id": b"\x07\x00\x9B\x3E", "device-id": b"\x9B\x3E\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"}, "8086:3e9b": {"AAPL,ig-platform-id": b"\x07\x00\x9B\x3E", "device-id": b"\x9B\x3E\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"},
# Kaby Lake Desktop (HD 630) - Common # Kaby Lake Desktop (HD 630)
"8086:5912": {"AAPL,ig-platform-id": b"\x05\x00\x12\x59", "device-id": b"\x12\x59\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"}, "8086:5912": {"AAPL,ig-platform-id": b"\x05\x00\x12\x59", "device-id": b"\x12\x59\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"},
# Skylake Desktop (HD 530) - Common # Skylake Desktop (HD 530)
"8086:1912": {"AAPL,ig-platform-id": b"\x00\x00\x12\x19", "device-id": b"\x12\x19\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"}, "8086:1912": {"AAPL,ig-platform-id": b"\x00\x00\x12\x19", "device-id": b"\x12\x19\x00\x00", "framebuffer-patch-enable": b"\x01\x00\x00\x00"},
# Alder Lake-S Desktop (UHD 730/750/770) - device-id often needs to be accurate # Alder Lake-S Desktop iGPUs (e.g., UHD 730, UHD 770)
"8086:4680": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x80\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i9-12900K UHD 770 (0x4680) -> common platform ID for iGPU only # For driving a display (Desktop): AAPL,ig-platform-id = 0x469B000A (Data: 0A009B46) or 0x4692000A (Data: 0A009246)
"8086:4690": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x90\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i5-12600K UHD 770 (0x4690) # device-id is often the PCI device ID itself, byte-swapped. e.g., 0x4690 -> <90460000>
"8086:4692": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # e.g. i5-12400 UHD 730 (0x4692) "8086:4690": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x90\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # For i5-12600K UHD 770
# Alternative Alder Lake platform-id (often when dGPU is primary) "8086:4680": {"AAPL,ig-platform-id": b"\x0A\x00\x9B\x46", "device-id": b"\x80\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # For i7/i9 UHD 770
"8086:4680_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x80\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # Using a suffix for internal logic, not a real PCI ID "8086:4692": {"AAPL,ig-platform-id": b"\x0A\x00\x92\x46", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # For i5 (non-K) UHD 730/770
"8086:4690_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x90\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, # Headless mode (if dGPU is primary) for Alder Lake: AAPL,ig-platform-id = 0x04001240 (Data: 04001240)
"8086:4692_dgpu": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x92\x46\x00\x00", "enable-hdmi20": b"\x01\x00\x00\x00"}, "8086:4690_headless": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x90\x46\x00\x00"},
"8086:4680_headless": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x80\x46\x00\x00"},
"8086:4692_headless": {"AAPL,ig-platform-id": b"\x04\x00\x12\x40", "device-id": b"\x92\x46\x00\x00"},
} }
INTEL_IGPU_PCI_PATH = "PciRoot(0x0)/Pci(0x2,0x0)" INTEL_IGPU_PCI_PATH = "PciRoot(0x0)/Pci(0x2,0x0)"
# Primary keys are now Codec Names. PCI IDs are secondary/fallback. # Primary keys are now Codec Names. PCI IDs are secondary/fallback.
AUDIO_LAYOUTS = { AUDIO_LAYOUTS = {
# Codec Names (Prefer these) - Extracted from "Codec: Realtek ALCXXX" or similar # Codec Names (from /proc/asound or lshw)
"Realtek ALC221": 11, "Realtek ALC233": 11, "Realtek ALC235": 28, "Realtek ALC221": 11, "Realtek ALC233": 11, "Realtek ALC235": 28,
"Realtek ALC255": 11, "Realtek ALC256": 11, "Realtek ALC257": 11, "Realtek ALC255": 11, "Realtek ALC256": 11, "Realtek ALC257": 11,
"Realtek ALC269": 11, "Realtek ALC271": 11, "Realtek ALC282": 11, "Realtek ALC269": 11, "Realtek ALC271": 11, "Realtek ALC282": 11,
@ -54,12 +54,12 @@ AUDIO_LAYOUTS = {
"Realtek ALC295": 11, "Realtek ALC295": 11,
"Realtek ALC662": 5, "Realtek ALC671": 11, "Realtek ALC662": 5, "Realtek ALC671": 11,
"Realtek ALC887": 7, "Realtek ALC888": 7, "Realtek ALC887": 7, "Realtek ALC888": 7,
"Realtek ALC892": 1, "Realtek ALC897": 11, # Common, 11 often works "Realtek ALC892": 1, "Realtek ALC897": 11, # Common for B660/B760, layout 11 or 66 often suggested
"Realtek ALC1150": 1, "Realtek ALC1150": 1,
"Realtek ALC1200": 7, "Realtek ALC1200": 7,
"Realtek ALC1220": 7, "Realtek ALC1220-VB": 7, # VB variant often uses same layouts "Realtek ALC1220": 7, "Realtek ALC1220-VB": 7, # VB variant often uses same layouts
"Conexant CX20756": 3, # Example Conexant "Conexant CX20756": 3, # Example Conexant
# Fallback PCI IDs for generic Intel HDA controllers if codec name not matched # Fallback PCI IDs for generic Intel HDA controllers
"pci_8086:a170": 1, # Sunrise Point-H HD Audio "pci_8086:a170": 1, # Sunrise Point-H HD Audio
"pci_8086:a2f0": 1, # Series 200 HD Audio (Kaby Lake) "pci_8086:a2f0": 1, # Series 200 HD Audio (Kaby Lake)
"pci_8086:a348": 3, # Cannon Point-LP HD Audio "pci_8086:a348": 3, # Cannon Point-LP HD Audio
@ -71,14 +71,13 @@ AUDIO_PCI_PATH_FALLBACK = "PciRoot(0x0)/Pci(0x1f,0x3)"
ETHERNET_KEXT_MAP = { # vendor_id:device_id -> kext_name ETHERNET_KEXT_MAP = { # vendor_id:device_id -> kext_name
"8086:15b8": "IntelMausi.kext", "8086:153a": "IntelMausi.kext", "8086:10f0": "IntelMausi.kext", "8086:15b8": "IntelMausi.kext", "8086:153a": "IntelMausi.kext", "8086:10f0": "IntelMausi.kext",
"8086:15be": "IntelMausi.kext", "8086:0d4f": "IntelMausi.kext", "8086:15b7": "IntelMausi.kext", # I219-V(3) "8086:15be": "IntelMausi.kext", "8086:0d4f": "IntelMausi.kext", "8086:15b7": "IntelMausi.kext", # I219-V variants
"8086:1a1c": "IntelMausi.kext", # Comet Lake-S vPro (I219-LM) "8086:1a1c": "IntelMausi.kext", # Comet Lake-S vPro (I219-LM)
"10ec:8168": "RealtekRTL8111.kext", "10ec:8111": "RealtekRTL8111.kext", "10ec:8168": "RealtekRTL8111.kext", "10ec:8111": "RealtekRTL8111.kext",
"10ec:2502": "LucyRTL8125Ethernet.kext", # Realtek RTL8125 2.5GbE "10ec:2502": "LucyRTL8125Ethernet.kext", # Realtek RTL8125 2.5GbE
"10ec:2600": "LucyRTL8125Ethernet.kext", # Realtek RTL8125B 2.5GbE "10ec:2600": "LucyRTL8125Ethernet.kext", # Realtek RTL8125B 2.5GbE
"8086:15ec": "AppleIntelI210Ethernet.kext", # I225-V (Often needs AppleIGB.kext or specific patches) "8086:15ec": "AppleIntelI210Ethernet.kext", # I225-V (Often needs AppleIGB.kext or specific patches)
"8086:15f3": "AppleIntelI210Ethernet.kext", # I225-V / I226-V "8086:15f3": "AppleIntelI210Ethernet.kext", # I225-V / I226-V
"14e4:1686": "AirportBrcmFixup.kext", # Placeholder for Broadcom Wi-Fi, actual kext depends on model
} }
@ -86,7 +85,6 @@ def enhance_config_plist(plist_path: str, target_macos_version_name: str, progre
def _report(msg): def _report(msg):
if progress_callback: progress_callback(f"[PlistModifier] {msg}") if progress_callback: progress_callback(f"[PlistModifier] {msg}")
else: print(f"[PlistModifier] {msg}") else: print(f"[PlistModifier] {msg}")
# ... (backup logic same as before) ...
_report(f"Starting config.plist enhancement for: {plist_path}"); _report(f"Target macOS version: {target_macos_version_name.lower()}") _report(f"Starting config.plist enhancement for: {plist_path}"); _report(f"Target macOS version: {target_macos_version_name.lower()}")
if not os.path.exists(plist_path): _report(f"Error: Plist file not found at {plist_path}"); return False if not os.path.exists(plist_path): _report(f"Error: Plist file not found at {plist_path}"); return False
backup_plist_path = plist_path + ".backup" backup_plist_path = plist_path + ".backup"
@ -115,23 +113,28 @@ def enhance_config_plist(plist_path: str, target_macos_version_name: str, progre
# 1. Intel iGPU # 1. Intel iGPU
intel_igpu_on_host = next((dev for dev in pci_devices if dev['type'] == 'VGA' and dev['vendor_id'] == '8086'), None) intel_igpu_on_host = next((dev for dev in pci_devices if dev['type'] == 'VGA' and dev['vendor_id'] == '8086'), None)
# Check for any discrete GPU (non-Intel VGA)
dgpu_present = any(dev['type'] == 'VGA' and dev['vendor_id'] != '8086' for dev in pci_devices) dgpu_present = any(dev['type'] == 'VGA' and dev['vendor_id'] != '8086' for dev in pci_devices)
if intel_igpu_on_host: if intel_igpu_on_host:
lookup_key = f"{intel_igpu_on_host['vendor_id']}:{intel_igpu_on_host['device_id']}" lookup_key = f"{intel_igpu_on_host['vendor_id']}:{intel_igpu_on_host['device_id']}"
# For Alder Lake, if a dGPU is also present, a different platform-id might be preferred. # If a dGPU is also present, prefer headless iGPU setup if available.
if lookup_key.startswith("8086:46") and dgpu_present: # Basic check for Alder Lake iGPU + dGPU final_lookup_key = lookup_key
lookup_key_dgpu = f"{lookup_key}_dgpu" if dgpu_present and f"{lookup_key}_headless" in INTEL_IGPU_DEFAULTS:
if lookup_key_dgpu in INTEL_IGPU_DEFAULTS: final_lookup_key = f"{lookup_key}_headless"
lookup_key = lookup_key_dgpu _report(f"Intel iGPU ({intel_igpu_on_host['description']}) detected with a dGPU. Applying headless properties: {final_lookup_key}")
_report(f"Intel Alder Lake iGPU ({intel_igpu_on_host['description']}) detected with a dGPU. Using dGPU-specific properties if available.") elif lookup_key in INTEL_IGPU_DEFAULTS:
_report(f"Intel iGPU ({intel_igpu_on_host['description']}) detected. Applying display properties: {lookup_key}")
else:
_report(f"Found Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}) but no default properties in map for key '{final_lookup_key}'.")
final_lookup_key = None # Ensure we don't use a key that's not in the map
if lookup_key in INTEL_IGPU_DEFAULTS: if final_lookup_key and final_lookup_key in INTEL_IGPU_DEFAULTS:
_report(f"Applying properties for Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}).")
igpu_path_properties = dev_props.setdefault(INTEL_IGPU_PCI_PATH, {}) igpu_path_properties = dev_props.setdefault(INTEL_IGPU_PCI_PATH, {})
for key, value in INTEL_IGPU_DEFAULTS[lookup_key].items(): for key, value in INTEL_IGPU_DEFAULTS[final_lookup_key].items():
if igpu_path_properties.get(key) != value: igpu_path_properties[key] = value; _report(f" Set {INTEL_IGPU_PCI_PATH} -> {key}"); modified_plist = True if igpu_path_properties.get(key) != value: igpu_path_properties[key] = value; _report(f" Set {INTEL_IGPU_PCI_PATH} -> {key}"); modified_plist = True
else: _report(f"Found Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}) but no default properties in map.") # else: already reported no properties found
# 2. Audio Enhancement - Prioritize detected codec name # 2. Audio Enhancement - Prioritize detected codec name
audio_device_pci_path_to_patch = AUDIO_PCI_PATH_FALLBACK # Default audio_device_pci_path_to_patch = AUDIO_PCI_PATH_FALLBACK # Default
@ -139,43 +142,26 @@ def enhance_config_plist(plist_path: str, target_macos_version_name: str, progre
if audio_codecs_detected: if audio_codecs_detected:
_report(f"Detected audio codecs: {audio_codecs_detected}") _report(f"Detected audio codecs: {audio_codecs_detected}")
for codec_name_full in audio_codecs_detected: for codec_name_full in audio_codecs_detected:
# Try to match known parts of codec names, e.g. "Realtek ALC897" from "Codec: Realtek ALC897"
# Or "ALC897" if that's how it's stored in AUDIO_LAYOUTS keys
for known_codec_key, layout_id in AUDIO_LAYOUTS.items(): for known_codec_key, layout_id in AUDIO_LAYOUTS.items():
if not known_codec_key.startswith("pci_"): # Ensure we are checking codec names, not PCI IDs if not known_codec_key.startswith("pci_"):
# Simple substring match or more specific regex # Try to match the core part of the codec name
# Example: "Realtek ALC255" should match "ALC255" if key is "ALC255" # e.g. "Realtek ALC897" should match a key like "ALC897" or "Realtek ALC897"
# Or if key is "Realtek ALC255" it matches directly if known_codec_key.lower() in codec_name_full.lower():
# For "Codec: Realtek ALC255" we might want to extract "Realtek ALC255" _report(f"Matched Audio Codec: '{codec_name_full}' (using key '{known_codec_key}'). Setting layout-id {layout_id}."); audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {})
# Attempt to extract the core codec part (e.g., "ALC255", "CX20756")
simple_codec_name_match = re.search(r"(ALC\d{3,4}(?:-VB)?|CX\d{4,})", codec_name_full, re.IGNORECASE)
simple_codec_name = simple_codec_name_match.group(1) if simple_codec_name_match else None
if (known_codec_key in codec_name_full) or \
(simple_codec_name and known_codec_key in simple_codec_name) or \
(known_codec_key.replace("Realtek ", "") in codec_name_full.replace("Realtek ", "")): # Try matching without "Realtek "
_report(f"Matched Audio Codec: '{codec_name_full}' (using key '{known_codec_key}'). Setting layout-id to {layout_id}.")
audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {})
new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little')) new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little'))
if audio_path_properties.get("layout-id") != new_layout_data: if audio_path_properties.get("layout-id") != new_layout_data: audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True
audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True
audio_layout_set = True; break audio_layout_set = True; break
if audio_layout_set: break if audio_layout_set: break
if not audio_layout_set: # Fallback to PCI ID of audio controller if not audio_layout_set:
_report("No specific audio codec match found or no codecs detected. Falling back to PCI ID for audio controller.") _report("No specific audio codec match found or no codecs detected. Falling back to PCI ID for audio controller.")
for dev in pci_devices: for dev in pci_devices:
if dev['type'] == 'Audio': if dev['type'] == 'Audio':
lookup_key = f"pci_{dev['vendor_id']}:{dev['device_id']}" # PCI ID keys are prefixed lookup_key = f"pci_{dev['vendor_id']}:{dev['device_id']}" # PCI ID keys are prefixed
if lookup_key in AUDIO_LAYOUTS: if lookup_key in AUDIO_LAYOUTS:
layout_id = AUDIO_LAYOUTS[lookup_key] layout_id = AUDIO_LAYOUTS[lookup_key]; _report(f"Found Audio (PCI): {dev['description']}. Setting layout-id {layout_id} via PCI ID map."); audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {})
_report(f"Found Audio device (PCI): {dev['description']}. Setting layout-id to {layout_id} via PCI ID map.")
audio_path_properties = dev_props.setdefault(audio_device_pci_path_to_patch, {})
new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little')) new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little'))
if audio_path_properties.get("layout-id") != new_layout_data: if audio_path_properties.get("layout-id") != new_layout_data: audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True
audio_path_properties["layout-id"] = new_layout_data; _report(f" Set {audio_device_pci_path_to_patch} -> layout-id = {layout_id}"); modified_plist = True
audio_layout_set = True; break audio_layout_set = True; break
if audio_layout_set: # Common action if any layout was set if audio_layout_set: # Common action if any layout was set
@ -200,53 +186,53 @@ def enhance_config_plist(plist_path: str, target_macos_version_name: str, progre
break break
# 4. NVIDIA GTX 970 Specific Adjustments # 4. NVIDIA GTX 970 Specific Adjustments
gtx_970_present = any(dev['vendor_id'] == '10de' and dev['device_id'] == '13c2' for dev in pci_devices) nvidia_gtx_970_present = any(dev['vendor_id'] == '10de' and dev['device_id'] == '13c2' for dev in pci_devices)
if gtx_970_present: if nvidia_gtx_970_present:
_report("NVIDIA GTX 970 detected.") _report("NVIDIA GTX 970 detected.")
high_sierra_and_older_versions = ["high sierra", "sierra", "el capitan"] high_sierra_versions = ["high sierra", "sierra"];
is_high_sierra_or_older_target = target_macos_version_name.lower() in high_sierra_and_older_versions is_legacy_nvidia_target = target_macos_version_name.lower() in high_sierra_versions
original_boot_args_set = set(boot_args) original_boot_args_set = set(boot_args)
if is_high_sierra_or_older_target: if is_legacy_nvidia_target:
boot_args.add('nvda_drv=1'); boot_args.discard('nv_disable=1') boot_args.add('nvda_drv=1'); boot_args.discard('nv_disable=1')
_report(" Configured for NVIDIA Web Drivers (High Sierra or older target).") _report(" Configured for NVIDIA Web Drivers (High Sierra or older target).")
else: # Mojave and newer else: # Mojave and newer
boot_args.discard('nvda_drv=1') boot_args.discard('nvda_drv=1')
boot_args.add('amfi_get_out_of_my_way=0x1') # For OCLP compatibility
_report(f" Added amfi_get_out_of_my_way=0x1 for {target_macos_version_name} (OCLP prep).")
if intel_igpu_on_host: if intel_igpu_on_host:
boot_args.add('nv_disable=1') boot_args.add('nv_disable=1')
_report(f" Added nv_disable=1 for {target_macos_version_name} to prioritize detected host iGPU over GTX 970.") _report(f" Added nv_disable=1 for {target_macos_version_name} to prioritize detected host iGPU over GTX 970.")
else: else:
boot_args.discard('nv_disable=1') boot_args.discard('nv_disable=1')
_report(f" GTX 970 is likely only GPU. `nv_disable=1` not forced for {target_macos_version_name}. Basic display expected.") _report(f" GTX 970 is primary GPU. `nv_disable=1` not forced for {target_macos_version_name}. Basic display expected. OCLP recommended post-install for acceleration.")
if boot_args != original_boot_args_set: modified_plist = True if boot_args != original_boot_args_set: modified_plist = True
final_boot_args_str = ' '.join(sorted(list(boot_args))) final_boot_args_str = ' '.join(sorted(list(boot_args)))
if boot_args_section.get('boot-args') != final_boot_args_str: if boot_args_section.get('boot-args') != final_boot_args_str:
boot_args_section['boot-args'] = final_boot_args_str boot_args_section['boot-args'] = final_boot_args_str
_report(f"Updated boot-args to: '{final_boot_args_str}'") _report(f"Updated boot-args to: '{final_boot_args_str}'"); modified_plist = True
modified_plist = True
if not modified_plist: if not modified_plist:
_report("No changes made to config.plist based on detected hardware or existing settings were different from defaults.") _report("No new modifications made to config.plist based on detected hardware or existing settings were different from defaults.")
# If no hardware changes on non-Linux, this is expected. if platform.system() != "Linux" and not pci_devices : return True
if platform.system() != "Linux" and not pci_devices : return True # No error, just no action
try: try: # Save logic (same as before)
with open(plist_path, 'wb') as f: with open(plist_path, 'wb') as f: plistlib.dump(config_data, f, sort_keys=True, fmt=plistlib.PlistFormat.XML)
plistlib.dump(config_data, f, sort_keys=True, fmt=plistlib.PlistFormat.XML) # Ensure XML format _report(f"Successfully saved config.plist to {plist_path}"); return True
_report(f"Successfully saved config.plist to {plist_path}") except Exception as e:
return True
except Exception as e: # ... (restore backup logic same as before)
_report(f"Error saving modified plist file {plist_path}: {e}") _report(f"Error saving modified plist file {plist_path}: {e}")
try: shutil.copy2(backup_plist_path, plist_path); _report("Restored backup successfully.") try: shutil.copy2(backup_plist_path, plist_path); _report("Restored backup successfully.")
except Exception as backup_error: _report(f"CRITICAL: FAILED TO RESTORE BACKUP. {plist_path} may be corrupt. Backup is at {backup_plist_path}. Error: {backup_error}") except Exception as backup_error: _report(f"CRITICAL: FAILED TO RESTORE BACKUP: {backup_error}")
return False return False
# if __name__ == '__main__': (Keep the same test block as before, ensure dummy data for kexts is complete) # if __name__ == '__main__': (Keep comprehensive test block)
if __name__ == '__main__': if __name__ == '__main__':
print("Plist Modifier Standalone Test") # ... (rest of test block as in previous version) import traceback # Ensure traceback is imported for standalone test
print("Plist Modifier Standalone Test") # ... (rest of test block as in previous version, ensure dummy data for kexts is complete)
dummy_plist_path = "test_config.plist" dummy_plist_path = "test_config.plist"
# Ensure kext entries in dummy_data have all required fields for the modifier logic to not error out
# when trying to access keys like "Enabled", "Arch", etc.
dummy_data = { dummy_data = {
"DeviceProperties": {"Add": {}}, "DeviceProperties": {"Add": {}},
"Kernel": {"Add": [ "Kernel": {"Add": [
@ -266,27 +252,26 @@ if __name__ == '__main__':
if platform.system() != "Linux": if platform.system() != "Linux":
print("Mocking hardware info for non-Linux.") print("Mocking hardware info for non-Linux.")
get_pci_devices_info = lambda: [ get_pci_devices_info = lambda: [
{'type': 'VGA', 'vendor_id': '8086', 'device_id': '4680', 'description': 'Alder Lake UHD 770', 'full_lspci_line':''}, {'type': 'VGA', 'vendor_id': '8086', 'device_id': '4690', 'description': 'Alder Lake UHD 770 (i5-12600K)', 'full_lspci_line':''},
{'type': 'Audio', 'vendor_id': '8086', 'device_id': '7ad0', 'description': 'Alder Lake PCH-P HD Audio', 'full_lspci_line':''}, {'type': 'Audio', 'vendor_id': '8086', 'device_id': '7ad0', 'description': 'Alder Lake PCH-P HD Audio', 'full_lspci_line':''},
{'type': 'Ethernet', 'vendor_id': '10ec', 'device_id': '2502', 'description': 'Realtek RTL8125', 'full_lspci_line':''}, {'type': 'Ethernet', 'vendor_id': '10ec', 'device_id': '2502', 'description': 'Realtek RTL8125 2.5GbE', 'full_lspci_line':''},
{'type': 'VGA', 'vendor_id': '10de', 'device_id': '13c2', 'description': 'NVIDIA GTX 970', 'full_lspci_line':''} # Test GTX 970 present
] ]
get_cpu_info = lambda: {"Model name": "12th Gen Intel(R) Core(TM) i7-12700K", "Flags": "avx avx2"} get_cpu_info = lambda: {"Model name": "12th Gen Intel(R) Core(TM) i7-12700K", "Flags": "avx avx2"}
get_audio_codecs = lambda: ["Realtek ALC1220", "Intel Alder Lake-S HDMI"] get_audio_codecs = lambda: ["Realtek ALC897", "Intel Alder Lake-S HDMI"] # Mock ALC897 for B760M
print("\n--- Testing with Sonoma (GTX 970 + iGPU present) ---")
print("\n--- Testing with Sonoma (should enable iGPU, audio [ALC1220 layout 7], ethernet [LucyRTL8125]) ---")
success_sonoma = enhance_config_plist(dummy_plist_path, "Sonoma", print) success_sonoma = enhance_config_plist(dummy_plist_path, "Sonoma", print)
print(f"Plist enhancement for Sonoma {'succeeded' if success_sonoma else 'failed'}.") print(f"Plist enhancement for Sonoma {'succeeded' if success_sonoma else 'failed'}.")
if success_sonoma: if success_sonoma:
with open(dummy_plist_path, 'rb') as f: modified_data = plistlib.load(f) with open(dummy_plist_path, 'rb') as f: modified_data = plistlib.load(f)
print(f" Sonoma boot-args: {modified_data.get('NVRAM',{}).get('Add',{}).get(boot_args_uuid,{}).get('boot-args')}") print(f" Sonoma boot-args: {modified_data.get('NVRAM',{}).get('Add',{}).get(boot_args_uuid,{}).get('boot-args')}") # Should have nv_disable=1, amfi
print(f" Sonoma iGPU props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(INTEL_IGPU_PCI_PATH)}") print(f" Sonoma iGPU props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(INTEL_IGPU_PCI_PATH)}") # Should have Alder Lake props (headless if dGPU active)
print(f" Sonoma Audio props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(AUDIO_PCI_PATH_FALLBACK)}") print(f" Sonoma Audio props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(AUDIO_PCI_PATH_FALLBACK)}") # Should have ALC897 layout
for kext in modified_data.get("Kernel",{}).get("Add",[]): for kext in modified_data.get("Kernel",{}).get("Add",[]):
if "LucyRTL8125Ethernet.kext" in kext.get("BundlePath",""): print(f" LucyRTL8125Ethernet.kext Enabled: {kext.get('Enabled')}") if "LucyRTL8125Ethernet.kext" in kext.get("BundlePath",""): print(f" LucyRTL8125Ethernet.kext Enabled: {kext.get('Enabled')}")
if "AppleALC.kext" in kext.get("BundlePath",""): print(f" AppleALC.kext Enabled: {kext.get('Enabled')}") if "AppleALC.kext" in kext.get("BundlePath",""): print(f" AppleALC.kext Enabled: {kext.get('Enabled')}")
if platform.system() != "Linux": if platform.system() != "Linux":
get_pci_devices_info = original_get_pci; get_cpu_info = original_get_cpu; get_audio_codecs = original_get_audio_codecs get_pci_devices_info = original_get_pci; get_cpu_info = original_get_cpu; get_audio_codecs = original_get_audio_codecs

View File

@ -1,11 +1,11 @@
# usb_writer_linux.py (Significant Refactoring for Installer Creation) # usb_writer_linux.py (Refined asset copying)
import subprocess import subprocess
import os import os
import time import time
import shutil import shutil
import glob import glob
import re import re
import plistlib # For plist_modifier call, and potentially for InstallInfo.plist import plistlib
try: try:
from plist_modifier import enhance_config_plist from plist_modifier import enhance_config_plist
@ -13,25 +13,23 @@ except ImportError:
enhance_config_plist = None enhance_config_plist = None
print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled for USBWriterLinux.") print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled for USBWriterLinux.")
# Assume a basic OpenCore EFI template directory exists relative to this script
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
class USBWriterLinux: class USBWriterLinux:
def __init__(self, device: str, macos_download_path: str, def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""): target_macos_version: str = ""): # target_macos_version is display name e.g. "Sonoma"
self.device = device self.device = device
self.macos_download_path = macos_download_path self.macos_download_path = macos_download_path
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version # String name like "Sonoma" self.target_macos_version = target_macos_version
pid = os.getpid() pid = os.getpid()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"temp_efi_build_{pid}" self.temp_efi_build_dir = f"temp_efi_build_{pid}"
self.temp_shared_support_extract_dir = f"temp_shared_support_extract_{pid}" self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For extracting HFS from DMG
self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}" self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}"
self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}" self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}"
@ -39,7 +37,7 @@ class USBWriterLinux:
self.temp_files_to_clean = [self.temp_basesystem_hfs_path] self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
self.temp_dirs_to_clean = [ self.temp_dirs_to_clean = [
self.temp_efi_build_dir, self.mount_point_usb_esp, self.temp_efi_build_dir, self.mount_point_usb_esp,
self.mount_point_usb_macos_target, self.temp_shared_support_extract_dir self.mount_point_usb_macos_target, self.temp_dmg_extract_dir
] ]
def _report_progress(self, message: str): def _report_progress(self, message: str):
@ -52,9 +50,9 @@ class USBWriterLinux:
process = subprocess.run( process = subprocess.run(
command, check=check, capture_output=capture_output, text=True, timeout=timeout, command, check=check, capture_output=capture_output, text=True, timeout=timeout,
shell=shell, cwd=working_dir, shell=shell, cwd=working_dir,
creationflags=0 # No CREATE_NO_WINDOW on Linux creationflags=0
) )
if capture_output: # Log only if content exists if capture_output:
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
return process return process
@ -64,7 +62,7 @@ class USBWriterLinux:
def _cleanup_temp_files_and_dirs(self): def _cleanup_temp_files_and_dirs(self):
self._report_progress("Cleaning up temporary files and directories...") self._report_progress("Cleaning up temporary files and directories...")
for mp in self.temp_dirs_to_clean: # Unmount first for mp in self.temp_dirs_to_clean:
if os.path.ismount(mp): if os.path.ismount(mp):
self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15) self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15)
@ -82,49 +80,89 @@ class USBWriterLinux:
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)] missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps: if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. Please install them (e.g., hfsprogs, p7zip-full)." 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(msg); raise RuntimeError(msg)
self._report_progress("All critical dependencies for Linux USB installer creation found.") self._report_progress("All critical dependencies for Linux USB installer creation found.")
return True return True
def _find_source_file(self, patterns: list[str], description: str) -> str | None: def _get_gibmacos_product_folder(self) -> str:
"""Finds the first existing file matching a list of glob patterns within self.macos_download_path.""" """Heuristically finds the main product folder within gibMacOS downloads."""
self._report_progress(f"Searching for {description} in {self.macos_download_path}...") # gibMacOS often creates .../publicrelease/XXX - macOS [VersionName] [VersionNum]/
for pattern in patterns: # We need to find this folder.
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches _report = self._report_progress
found_files = glob.glob(os.path.join(self.macos_download_path, "**", pattern), recursive=True) _report(f"Searching for macOS product folder in {self.macos_download_path} for version {self.target_macos_version}")
if found_files:
# Prefer files not inside .app bundles if multiple are found, unless it's the app itself. version_parts = self.target_macos_version.split(" ") # e.g., "Sonoma" or "Mac OS X", "High Sierra"
# This is a simple heuristic. primary_name = version_parts[0] # "Sonoma", "Mac", "High"
non_app_files = [f for f in found_files if ".app/" not in f] if primary_name == "Mac" and len(version_parts) > 2 and version_parts[1] == "OS": # "Mac OS X"
target_file = non_app_files[0] if non_app_files else found_files[0] primary_name = "OS X"
self._report_progress(f"Found {description} at: {target_file}") if len(version_parts) > 2 and version_parts[2] == "X": primary_name = "OS X" # For "Mac OS X"
return target_file
self._report_progress(f"Warning: {description} not found with patterns: {patterns}") possible_folders = []
for root, dirs, _ in os.walk(self.macos_download_path):
for d_name in dirs:
# Check if directory name contains "macOS" and a part of the target version name/number
if "macOS" in d_name and (primary_name in d_name or self.target_macos_version in d_name):
possible_folders.append(os.path.join(root, d_name))
if not possible_folders:
_report(f"Could not automatically determine specific product folder. Using base download path: {self.macos_download_path}")
return self.macos_download_path
# Prefer shorter paths or more specific matches if multiple found
# This heuristic might need refinement. For now, take the first plausible one.
_report(f"Found potential product folder(s): {possible_folders}. Using: {possible_folders[0]}")
return possible_folders[0]
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder: str, description: str) -> str | None:
"""Finds the first existing file matching a list of glob patterns within the product_folder."""
if isinstance(asset_patterns, str): asset_patterns = [asset_patterns]
self._report_progress(f"Searching for {description} using patterns {asset_patterns} in {product_folder}...")
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)
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]}")
return found_files[0]
self._report_progress(f"Warning: {description} not found with patterns: {asset_patterns} in {product_folder} or its subdirectories.")
return None return None
def _extract_hfs_from_dmg(self, dmg_path: str, output_hfs_path: str) -> bool: 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 DMG.""" """Extracts the primary HFS+ partition image (e.g., '4.hfs') from a source DMG (BaseSystem.dmg or InstallESD.dmg)."""
# Assumes BaseSystem.dmg or similar that contains a HFS+ partition image. os.makedirs(self.temp_dmg_extract_dir, exist_ok=True)
temp_extract_dir = f"temp_hfs_extract_{os.getpid()}"
os.makedirs(temp_extract_dir, exist_ok=True)
try: try:
self._report_progress(f"Extracting HFS+ partition image from {dmg_path}...") 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) # 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar for BaseSystem)
self._run_command(["7z", "e", "-tdmg", dmg_path, "*.hfs", f"-o{temp_extract_dir}"], check=True) # 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(temp_extract_dir, "*.hfs")) hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"))
if not hfs_files: raise RuntimeError(f"No .hfs file found after extracting {dmg_path}") 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 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}") self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}")
shutil.move(final_hfs_file, output_hfs_path) shutil.move(final_hfs_file, output_hfs_path) # Use shutil.move for local files
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"Error during HFS extraction from DMG: {e}") self._report_progress(f"Error during HFS extraction from DMG: {e}\n{traceback.format_exc()}")
return False return False
finally: finally:
if os.path.exists(temp_extract_dir): shutil.rmtree(temp_extract_dir, ignore_errors=True) if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True)
def format_and_write(self) -> bool: def format_and_write(self) -> bool:
try: try:
@ -139,7 +177,7 @@ class USBWriterLinux:
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...") self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
self._run_command(["sudo", "sgdisk", "--zap-all", self.device]) self._run_command(["sudo", "sgdisk", "--zap-all", self.device])
self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device]) self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device])
self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", "2:Install macOS", 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", "partprobe", self.device], timeout=10); time.sleep(3) self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3)
esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None) esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None)
@ -152,21 +190,17 @@ class USBWriterLinux:
self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev]) self._run_command(["sudo", "mkfs.hfsplus", "-v", f"Install macOS {self.target_macos_version}", macos_partition_dev])
# --- Prepare macOS Installer Content --- # --- Prepare macOS Installer Content ---
basesystem_dmg_path = self._find_source_file(["BaseSystem.dmg", "InstallAssistant.pkg", "SharedSupport.dmg"], "BaseSystem.dmg or InstallAssistant.pkg or SharedSupport.dmg") product_folder = self._get_gibmacos_product_folder()
if not basesystem_dmg_path: raise RuntimeError("Essential macOS installer DMG/PKG not found in download path.")
if basesystem_dmg_path.endswith(".pkg") or "SharedSupport.dmg" in os.path.basename(basesystem_dmg_path) : # Find BaseSystem.dmg (or equivalent like InstallESD.dmg if BaseSystem.dmg is not directly available)
# If we found InstallAssistant.pkg or SharedSupport.dmg, we need to extract BaseSystem.hfs from it. # Some gibMacOS downloads might have InstallESD.dmg which contains BaseSystem.dmg.
self._report_progress(f"Extracting bootable HFS+ image from {basesystem_dmg_path}...") # Others might have BaseSystem.dmg directly.
if not self._extract_hfs_from_dmg(basesystem_dmg_path, self.temp_basesystem_hfs_path): 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)")
raise RuntimeError("Failed to extract HFS+ image from installer assets.") if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
elif basesystem_dmg_path.endswith("BaseSystem.dmg"): # If it's BaseSystem.dmg directly
self._report_progress(f"Extracting bootable HFS+ image from {basesystem_dmg_path}...")
if not self._extract_hfs_from_dmg(basesystem_dmg_path, self.temp_basesystem_hfs_path):
raise RuntimeError("Failed to extract HFS+ image from BaseSystem.dmg.")
else:
raise RuntimeError(f"Unsupported file type for BaseSystem extraction: {basesystem_dmg_path}")
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.")
self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...") 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"]) self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"])
@ -174,58 +208,72 @@ class USBWriterLinux:
self._report_progress("Mounting macOS Install partition on USB...") self._report_progress("Mounting macOS Install partition on USB...")
self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target]) self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target])
# Copy BaseSystem.dmg & .chunklist to /System/Library/CoreServices/
core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices") 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]) self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
# Find original BaseSystem.dmg and chunklist in download path to copy them # Copy original BaseSystem.dmg and .chunklist from gibMacOS output
actual_bs_dmg = self._find_source_file(["BaseSystem.dmg"], "original BaseSystem.dmg for copying") original_bs_dmg = self._find_gibmacos_asset(["BaseSystem.dmg"], product_folder, "original BaseSystem.dmg")
if actual_bs_dmg: if original_bs_dmg:
self._report_progress(f"Copying {actual_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg") self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
self._run_command(["sudo", "cp", actual_bs_dmg, os.path.join(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.")
bs_chunklist = actual_bs_dmg.replace(".dmg", ".chunklist") install_info_src = self._find_gibmacos_asset(["InstallInfo.plist"], product_folder, "InstallInfo.plist")
if os.path.exists(bs_chunklist):
self._report_progress(f"Copying {bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
self._run_command(["sudo", "cp", bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
else: self._report_progress(f"Warning: BaseSystem.chunklist not found at {bs_chunklist}")
else: self._report_progress("Warning: Could not find original BaseSystem.dmg in download path to copy to CoreServices.")
# Copy InstallInfo.plist
install_info_src = self._find_source_file(["InstallInfo.plist"], "InstallInfo.plist")
if install_info_src: if install_info_src:
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist") 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")]) 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 download path.") else: self._report_progress("Warning: InstallInfo.plist not found in product folder.")
# Copy Packages (placeholder - needs more specific logic based on gibMacOS output structure) # Copy Packages and other assets
self._report_progress("Placeholder: Copying macOS installation packages to USB (e.g., /System/Installation/Packages)...") packages_target_path = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")
# Example: sudo rsync -a /path/to/downloaded_packages_dir/ /mnt/usb_macos_target/System/Installation/Packages/ self._run_command(["sudo", "mkdir", "-p", packages_target_path])
# This needs to correctly identify the source Packages directory from gibMacOS output.
# For now, we'll skip actual copying of packages folder, as its location and content can vary.
# A proper implementation would require inspecting the gibMacOS download structure.
# Create the directory though:
self._run_command(["sudo", "mkdir", "-p", os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")])
# 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.")
# --- OpenCore EFI Setup --- # 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...") self._report_progress("Setting up OpenCore EFI on ESP...")
if not os.path.isdir(OC_TEMPLATE_DIR): 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"FATAL: OpenCore template directory not found at {OC_TEMPLATE_DIR}. Cannot proceed."); return False
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}") 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]) # Copy contents 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") # Assume template is named config.plist temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
# If template is config-template.plist, rename it for enhancement
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")): if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
# If template is config-template.plist, rename it for enhancement self._run_command(["sudo", "mv", os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path])
shutil.move(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 self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._report_progress("Attempting to enhance config.plist...") self._report_progress("Attempting to enhance config.plist...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.")
self._report_progress("config.plist enhancement successful.") else: self._report_progress("config.plist enhancement failed or had issues.")
else: self._report_progress("config.plist enhancement failed or had issues. Continuing with (potentially original template) plist.")
self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp]) self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp])
self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...") self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...")
@ -235,41 +283,47 @@ class USBWriterLinux:
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"An error occurred during USB writing: {e}") self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}")
import traceback; self._report_progress(traceback.format_exc())
return False return False
finally: finally:
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
if __name__ == '__main__': if __name__ == '__main__':
if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1) if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1)
print("USB Writer Linux Standalone Test - Installer Method") print("USB Writer Linux Standalone Test - Installer Method (Refined)")
mock_download_dir = f"temp_macos_download_test_{os.getpid()}" mock_download_dir = f"temp_macos_download_test_{os.getpid()}"
os.makedirs(mock_download_dir, exist_ok=True) os.makedirs(mock_download_dir, exist_ok=True)
# Create a dummy placeholder for what gibMacOS might download # Create a more structured mock download similar to gibMacOS output
# This is highly simplified. A real gibMacOS download has a complex structure. product_name_slug = f"000-00000 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.0" # Example
# For this test, we'll simulate having BaseSystem.dmg and InstallInfo.plist specific_product_folder = os.path.join(mock_download_dir, "publicrelease", product_name_slug)
mock_install_data_path = os.path.join(mock_download_dir, "macOS_Install_Data") # Simplified path os.makedirs(specific_product_folder, exist_ok=True)
os.makedirs(mock_install_data_path, exist_ok=True)
dummy_bs_dmg_path = os.path.join(mock_install_data_path, "BaseSystem.dmg")
dummy_installinfo_path = os.path.join(mock_download_dir, "InstallInfo.plist") # Often at root of a specific product download
# 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): if not os.path.exists(dummy_bs_dmg_path):
# Create a tiny dummy file for 7z to "extract" from.
# To make _extract_hfs_from_dmg work, it needs a real DMG with a HFS part.
# This is hard to mock simply. For now, it will likely fail extraction.
# A better mock would be a small, actual DMG with a tiny HFS file.
print(f"Creating dummy BaseSystem.dmg at {dummy_bs_dmg_path} (will likely fail HFS extraction in test without a real DMG structure)")
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(1024*10)) # 10KB dummy
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "w") as f: f.write("<plist><dict><key>DummyInstallInfo</key><true/></dict></plist>")
# Create dummy EFI template # 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(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(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") # Name it config.plist directly dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
if not os.path.exists(dummy_config_template_path): if not os.path.exists(dummy_config_template_path):
with open(dummy_config_template_path, "w") as f: f.write("<plist><dict><key>TestTemplate</key><true/></dict></plist>") with open(dummy_config_template_path, "w") as f: f.write("<plist><dict><key>TestTemplate</key><true/></dict></plist>")
@ -285,18 +339,14 @@ if __name__ == '__main__':
if confirm.lower() == 'yes': if confirm.lower() == 'yes':
writer = USBWriterLinux( writer = USBWriterLinux(
device=test_device, device=test_device,
macos_download_path=mock_download_dir, macos_download_path=mock_download_dir, # Pass base download dir
progress_callback=print, progress_callback=print,
enhance_plist_enabled=True, enhance_plist_enabled=True,
target_macos_version="Sonoma" target_macos_version=sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
) )
success = writer.format_and_write() success = writer.format_and_write()
else: print("Test cancelled by user.") else: print("Test cancelled by user.")
print(f"Test finished. Success: {success}") print(f"Test finished. Success: {success}")
# Cleanup
if os.path.exists(mock_download_dir): shutil.rmtree(mock_download_dir, ignore_errors=True) if os.path.exists(mock_download_dir): shutil.rmtree(mock_download_dir, ignore_errors=True)
# if os.path.exists(OC_TEMPLATE_DIR) and "EFI_template_installer" in OC_TEMPLATE_DIR :
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Avoid deleting if it's a real shared template
print("Mock download dir cleaned up.") print("Mock download dir cleaned up.")
print(f"Note: {OC_TEMPLATE_DIR} and its contents might persist if not created by this test run specifically.")

View File

@ -1,316 +1,312 @@
# usb_writer_macos.py # usb_writer_macos.py (Refactoring for Installer Workflow)
import subprocess import subprocess
import os import os
import time import time
import shutil # For checking command existence import shutil
import plistlib # For parsing diskutil list -plist output import glob
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.")
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
class USBWriterMacOS: class USBWriterMacOS:
def __init__(self, device: str, opencore_qcow2_path: str, macos_qcow2_path: str, def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): # New args progress_callback=None, enhance_plist_enabled: bool = False,
self.device = device # Should be like /dev/diskX target_macos_version: str = ""):
self.opencore_qcow2_path = opencore_qcow2_path self.device = device # e.g., /dev/diskX
self.macos_qcow2_path = macos_qcow2_path self.macos_download_path = macos_download_path
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled # Store self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version # Store self.target_macos_version = target_macos_version
pid = os.getpid() pid = os.getpid()
self.opencore_raw_path = f"opencore_temp_{pid}.raw" self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs" # Use /tmp for macOS
self.macos_raw_path = f"macos_main_temp_{pid}.raw" self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}"
self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{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_usb_esp_mount = f"/tmp/usb_esp_temp_skyscope_{pid}"
self.temp_macos_source_mount = f"/tmp/macos_source_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_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_files_to_clean = [self.opencore_raw_path, self.macos_raw_path] self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
self.temp_mount_points_to_clean = [ self.temp_dirs_to_clean = [
self.temp_opencore_mount, self.temp_usb_esp_mount, self.temp_efi_build_dir, self.temp_opencore_mount,
self.temp_macos_source_mount, self.temp_usb_macos_target_mount self.temp_usb_esp_mount, self.temp_macos_source_mount,
self.temp_usb_macos_target_mount, self.temp_dmg_extract_dir
] ]
self.attached_raw_images_devices = [] # Store devices from hdiutil attach self.attached_dmg_devices = [] # Store devices from hdiutil attach
def _report_progress(self, message: str): def _report_progress(self, message: str): # ... (same)
print(message) # For standalone testing if self.progress_callback: self.progress_callback(message)
if self.progress_callback: else: print(message)
self.progress_callback(message)
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None): def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False): # ... (same)
self._report_progress(f"Executing: {' '.join(command)}") self._report_progress(f"Executing: {' '.join(command)}")
try: try:
process = subprocess.run( process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell)
command, check=check, capture_output=capture_output, text=True, timeout=timeout
)
if capture_output: if capture_output:
if process.stdout and process.stdout.strip(): if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
self._report_progress(f"STDOUT: {process.stdout.strip()}") if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
if process.stderr and process.stderr.strip():
self._report_progress(f"STDERR: {process.stderr.strip()}")
return process return process
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
self._report_progress(f"Command {' '.join(command)} timed out after {timeout} seconds.") except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
raise except FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise
except subprocess.CalledProcessError as e:
self._report_progress(f"Error executing {' '.join(command)} (code {e.returncode}): {e.stderr or e.stdout or str(e)}")
raise
except FileNotFoundError:
self._report_progress(f"Error: Command '{command[0]}' not found. Is it installed and in PATH?")
raise
def _cleanup_temp_files(self): def _cleanup_temp_files_and_dirs(self): # Updated for macOS
self._report_progress("Cleaning up temporary image files...") self._report_progress("Cleaning up temporary files and directories...")
for f_path in self.temp_files_to_clean: for f_path in self.temp_files_to_clean:
if os.path.exists(f_path): if os.path.exists(f_path):
try: try: os.remove(f_path) # No sudo needed for /tmp files usually
os.remove(f_path) except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
self._report_progress(f"Removed {f_path}")
except OSError as e:
self._report_progress(f"Error removing {f_path}: {e}")
def _unmount_path(self, mount_path_or_device, is_device=False, force=False): # Detach DMGs first
target = mount_path_or_device for dev_path in list(self.attached_dmg_devices): # Iterate copy
cmd_base = ["diskutil"] self._detach_dmg(dev_path)
action = "unmountDisk" if is_device else "unmount" self.attached_dmg_devices = []
if force: for d_path in self.temp_dirs_to_clean:
cmd = cmd_base + [action, "force", target] if os.path.ismount(d_path):
else: try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30)
cmd = cmd_base + [action, target] except Exception: pass # Ignore if already unmounted or error
if os.path.exists(d_path):
try: shutil.rmtree(d_path, ignore_errors=True)
except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
is_target_valid_for_unmount = (os.path.ismount(mount_path_or_device) and not is_device) or \ def _detach_dmg(self, device_path_or_mount_point):
(is_device and os.path.exists(target)) 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)
if is_target_valid_for_unmount: except Exception as e:
self._report_progress(f"Attempting to unmount {target} (Action: {action}, Force: {force})...") self._report_progress(f"Could not detach {device_path_or_mount_point}: {e}")
self._run_command(cmd, check=False, timeout=30)
def _detach_raw_image_device(self, device_path):
if device_path and os.path.exists(device_path):
self._report_progress(f"Detaching raw image device {device_path}...")
try:
info_check = subprocess.run(["diskutil", "info", device_path], capture_output=True, text=True, check=False)
if info_check.returncode == 0:
self._run_command(["hdiutil", "detach", device_path, "-force"], check=False, timeout=30)
else:
self._report_progress(f"Device {device_path} appears invalid or already detached.")
except Exception as e:
self._report_progress(f"Exception while checking/detaching {device_path}: {e}")
def _cleanup_all_mounts_and_mappings(self):
self._report_progress("Cleaning up all temporary mounts and attached raw images...")
for mp in reversed(self.temp_mount_points_to_clean):
self._unmount_path(mp, force=True)
if os.path.exists(mp):
try: os.rmdir(mp)
except OSError as e: self._report_progress(f"Could not rmdir {mp}: {e}")
devices_to_detach = list(self.attached_raw_images_devices)
for dev_path in devices_to_detach:
self._detach_raw_image_device(dev_path)
self.attached_raw_images_devices = []
def check_dependencies(self): def check_dependencies(self):
self._report_progress("Checking dependencies (qemu-img, diskutil, hdiutil, rsync)...") self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd)...")
dependencies = ["qemu-img", "diskutil", "hdiutil", "rsync"] dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"]
missing_deps = [] missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
for dep in dependencies:
if not shutil.which(dep):
missing_deps.append(dep)
if missing_deps: if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `qemu-img` might need to be installed (e.g., via Homebrew: `brew install qemu`). `diskutil`, `hdiutil`, `rsync` are usually standard on macOS." msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`)."
self._report_progress(msg) self._report_progress(msg); raise RuntimeError(msg)
raise RuntimeError(msg) self._report_progress("All critical dependencies for macOS USB installer creation found.")
self._report_progress("All critical dependencies found.")
return True return True
def _get_partition_device_id(self, parent_disk_id_str: str, partition_label_or_type: str) -> str | None: def _get_gibmacos_product_folder(self) -> str | None:
"""Finds partition device ID by Volume Name or Content Hint.""" base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease")
target_disk_id = parent_disk_id_str.replace("/dev/", "") if not os.path.isdir(base_path): base_path = self.macos_download_path
self._report_progress(f"Searching for partition '{partition_label_or_type}' on disk '{target_disk_id}'") 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
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
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:
# 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}.")
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: try:
result = self._run_command(["diskutil", "list", "-plist", target_disk_id], capture_output=True) if dmg_or_pkg_path.endswith(".pkg"):
if not result.stdout: self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True)
self._report_progress(f"No stdout from diskutil list for {target_disk_id}") dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg"));
return None 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}")
plist_data = plistlib.loads(result.stdout.encode('utf-8')) 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]
all_disks_and_partitions = plist_data.get("AllDisksAndPartitions", []) self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...")
if not isinstance(all_disks_and_partitions, list): self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
if plist_data.get("DeviceIdentifier") == target_disk_id: hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
all_disks_and_partitions = [plist_data] if not hfs_files: raise RuntimeError(f"No .hfs file found from {basesystem_dmg_to_process}")
else: 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
all_disks_and_partitions = [] 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)
for disk_info_entry in all_disks_and_partitions:
current_disk_id_in_plist = disk_info_entry.get("DeviceIdentifier")
if current_disk_id_in_plist == target_disk_id:
for part_info in disk_info_entry.get("Partitions", []):
vol_name = part_info.get("VolumeName")
content_hint = part_info.get("Content")
device_id = part_info.get("DeviceIdentifier")
if device_id: def _create_minimal_efi_template(self, efi_dir_path): # Same as linux version
if vol_name and vol_name.strip().lower() == partition_label_or_type.strip().lower(): self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}")
self._report_progress(f"Found partition by VolumeName: {vol_name} -> /dev/{device_id}") 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)
return f"/dev/{device_id}" for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True)
if content_hint and content_hint.strip().lower() == partition_label_or_type.strip().lower(): with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("")
self._report_progress(f"Found partition by Content type: {content_hint} -> /dev/{device_id}") with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("")
return f"/dev/{device_id}" basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}}
try:
with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML)
self._report_progress("Created basic placeholder config.plist.")
except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}")
self._report_progress(f"Partition '{partition_label_or_type}' not found on disk '{target_disk_id}'.")
return None
except Exception as e:
self._report_progress(f"Error parsing 'diskutil list -plist {target_disk_id}': {e}")
return None
def format_and_write(self) -> bool: def format_and_write(self) -> bool:
try: try:
self.check_dependencies() self.check_dependencies()
self._cleanup_all_mounts_and_mappings() self._cleanup_temp_files_and_dirs()
for mp_dir in self.temp_dirs_to_clean: # Use full list from constructor
for mp in self.temp_mount_points_to_clean: os.makedirs(mp_dir, exist_ok=True)
os.makedirs(mp, exist_ok=True)
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
self._report_progress(f"Unmounting disk {self.device} (force)...") self._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2)
self._unmount_path(self.device, is_device=True, force=True)
time.sleep(2)
self._report_progress(f"Partitioning {self.device} with GPT scheme...") installer_vol_name = f"Install macOS {self.target_macos_version}"
self._run_command([ self._report_progress(f"Partitioning {self.device} as GPT: EFI (FAT32, 551MB), '{installer_vol_name}' (HFS+)...")
"diskutil", "partitionDisk", self.device, "GPT", self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3)
"MS-DOS FAT32", "EFI", "551MiB",
"JHFS+", "macOS_USB", "0b"
], timeout=180)
time.sleep(3)
esp_partition_dev = self._get_partition_device_id(self.device, "EFI") # Get actual partition identifiers
macos_partition_dev = self._get_partition_device_id(self.device, "macOS_USB") 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.")
if not (esp_partition_dev and os.path.exists(esp_partition_dev)): disk_info = plistlib.loads(disk_info_plist.encode('utf-8'))
esp_partition_dev = f"{self.device}s1"
if not (macos_partition_dev and os.path.exists(macos_partition_dev)):
macos_partition_dev = f"{self.device}s2"
if not (os.path.exists(esp_partition_dev) and os.path.exists(macos_partition_dev)):
raise RuntimeError(f"Could not identify partitions on {self.device}. ESP: {esp_partition_dev}, macOS: {macos_partition_dev}")
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}).")
self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}") self._report_progress(f"Identified ESP: {esp_partition_dev}, macOS Partition: {macos_partition_dev}")
# --- Write EFI content --- # --- Prepare macOS Installer Content ---
self._report_progress(f"Converting OpenCore QCOW2 ({self.opencore_qcow2_path}) to RAW ({self.opencore_raw_path})...") product_folder = self._get_gibmacos_product_folder()
self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path]) source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
self._report_progress(f"Attaching RAW OpenCore image ({self.opencore_raw_path})...") if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path):
attach_cmd_efi = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.opencore_raw_path] raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.")
efi_attach_output = self._run_command(attach_cmd_efi, capture_output=True).stdout.strip()
raw_efi_disk_id = efi_attach_output.splitlines()[-1].strip().split()[0]
if not raw_efi_disk_id.startswith("/dev/disk"):
raise RuntimeError(f"Failed to attach raw EFI image: {efi_attach_output}")
self.attached_raw_images_devices.append(raw_efi_disk_id)
self._report_progress(f"Attached raw OpenCore image as {raw_efi_disk_id}")
time.sleep(2)
source_efi_partition_dev = self._get_partition_device_id(raw_efi_disk_id, "EFI") or f"{raw_efi_disk_id}s1" raw_macos_partition_dev = macos_partition_dev.replace("/dev/disk", "/dev/rdisk") # Use raw device for dd
self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...")
self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800)
self._report_progress(f"Mounting source EFI partition ({source_efi_partition_dev}) to {self.temp_opencore_mount}...") self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB...")
self._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_opencore_mount, source_efi_partition_dev], timeout=30) self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev])
self._report_progress(f"Mounting target USB ESP ({esp_partition_dev}) to {self.temp_usb_esp_mount}...") core_services_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev], timeout=30) self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
source_efi_content_path = os.path.join(self.temp_opencore_mount, "EFI") original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder)
if not os.path.isdir(source_efi_content_path): source_efi_content_path = self.temp_opencore_mount 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")])
target_efi_dir_on_usb = os.path.join(self.temp_usb_esp_mount, "EFI") install_info_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder)
self._report_progress(f"Copying EFI files from {source_efi_content_path} to {target_efi_dir_on_usb}...") if install_info_src:
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{source_efi_content_path}/", f"{target_efi_dir_on_usb}/"]) 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")])
self._unmount_path(self.temp_opencore_mount, force=True) packages_dir_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages")
self._unmount_path(self.temp_usb_esp_mount, force=True) self._run_command(["sudo", "mkdir", "-p", packages_dir_usb])
self._detach_raw_image_device(raw_efi_disk_id); raw_efi_disk_id = None
# --- Write macOS main image (File-level copy) --- # Copy main installer package(s) or app contents. This is simplified.
self._report_progress(f"Converting macOS QCOW2 ({self.macos_qcow2_path}) to RAW ({self.macos_raw_path})...") # A real createinstallmedia copies the .app then uses it. We are building manually.
self._report_progress("This may take a very long time...") # We need to find the main payload: InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg content.
self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path]) 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.")
self._report_progress(f"Attaching RAW macOS image ({self.macos_raw_path})...") self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")])
attach_cmd_macos = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.macos_raw_path] self._report_progress("macOS installer assets copied.")
macos_attach_output = self._run_command(attach_cmd_macos, capture_output=True).stdout.strip()
raw_macos_disk_id = macos_attach_output.splitlines()[-1].strip().split()[0]
if not raw_macos_disk_id.startswith("/dev/disk"):
raise RuntimeError(f"Failed to attach raw macOS image: {macos_attach_output}")
self.attached_raw_images_devices.append(raw_macos_disk_id)
self._report_progress(f"Attached raw macOS image as {raw_macos_disk_id}")
time.sleep(2)
source_macos_part_dev = self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS_Container") or \ # --- OpenCore EFI Setup ---
self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS") or \ self._report_progress("Setting up OpenCore EFI on ESP...")
self._get_partition_device_id(raw_macos_disk_id, "Apple_HFS") or \ 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)
f"{raw_macos_disk_id}s2" 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])
if not (source_macos_part_dev and os.path.exists(source_macos_part_dev)):
raise RuntimeError(f"Could not find source macOS partition on {raw_macos_disk_id}")
self._report_progress(f"Mounting source macOS partition ({source_macos_part_dev}) to {self.temp_macos_source_mount}...") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
self._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_macos_source_mount, source_macos_part_dev], timeout=60) 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")):
shutil.copy2(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path)
self._report_progress(f"Mounting target USB macOS partition ({macos_partition_dev}) to {self.temp_usb_macos_target_mount}...") if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev], timeout=30) 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.")
else: self._report_progress("config.plist enhancement call failed or had issues.")
self._report_progress(f"Copying macOS system files from {self.temp_macos_source_mount} to {self.temp_usb_macos_target_mount} (sudo rsync)...") self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
self._report_progress("This will also take a very long time.") self._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...")
self._run_command([ self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"])
"sudo", "rsync", "-avh", "--delete",
"--exclude=.Spotlight-V100", "--exclude=.fseventsd", "--exclude=/.Trashes", "--exclude=/System/Volumes/VM", "--exclude=/private/var/vm",
f"{self.temp_macos_source_mount}/", f"{self.temp_usb_macos_target_mount}/"
])
self._report_progress("USB writing process completed successfully.") self._report_progress("USB Installer creation process completed successfully.")
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"An error occurred during USB writing on macOS: {e}") self._report_progress(f"An error occurred during USB writing on macOS: {e}\n{traceback.format_exc()}")
import traceback
self._report_progress(traceback.format_exc())
return False return False
finally: finally:
self._cleanup_all_mounts_and_mappings() self._cleanup_temp_files_and_dirs()
self._cleanup_temp_files()
if __name__ == '__main__': if __name__ == '__main__':
if platform.system() != "Darwin": print("This script is intended for macOS."); exit(1) import traceback
print("USB Writer macOS Standalone Test - File Copy Method") 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"
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
mock_opencore_path = "mock_opencore_macos.qcow2" # Create dummy BaseSystem.dmg inside the product folder's SharedSupport
mock_macos_path = "mock_macos_macos.qcow2" dummy_bs_dmg_path = os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg")
if not os.path.exists(mock_opencore_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_opencore_path, "384M"]) if not os.path.exists(dummy_bs_dmg_path):
if not os.path.exists(mock_macos_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_macos_path, "1G"]) with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy DMG
print("\nAvailable disks (use 'diskutil list external physical' in Terminal to identify your USB):") dummy_installinfo_path = os.path.join(mock_product_folder_path, "InstallInfo.plist")
subprocess.run(["diskutil", "list", "external", "physical"], check=False) if not os.path.exists(dummy_installinfo_path):
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX - NOT /dev/diskXsY). THIS DISK WILL BE WIPED: ") 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 test_device or not test_device.startswith("/dev/disk"): if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
print("Invalid disk identifier. Exiting.") if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
if os.path.exists(mock_opencore_path): os.remove(mock_opencore_path) dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
if os.path.exists(mock_macos_path): os.remove(mock_macos_path) if not os.path.exists(dummy_config_template_path):
exit(1) with open(dummy_config_template_path, "wb") as f: plistlib.dump({"TestTemplate":True}, f)
confirm = input(f"Are you sure you want to wipe {test_device} and write mock images? (yes/NO): ") print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False)
success = False test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ")
if confirm.lower() == 'yes': 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
print("Ensure you have sudo privileges for rsync if needed, or app is run as root.") if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
writer = USBWriterMacOS(test_device, mock_opencore_path, mock_macos_path, print) writer = USBWriterMacOS(test_device, mock_download_dir, print, True, sys.argv[1] if len(sys.argv) > 1 else "Sonoma")
success = writer.format_and_write() writer.format_and_write()
else: else: print("Test cancelled.")
print("Test cancelled.") shutil.rmtree(mock_download_dir, ignore_errors=True)
print("Mock download dir cleaned up.")
print(f"Test finished. Success: {success}")
if os.path.exists(mock_opencore_path): os.remove(mock_opencore_path)
if os.path.exists(mock_macos_path): os.remove(mock_macos_path)
print("Mock files cleaned up.")

View File

@ -1,48 +1,58 @@
# usb_writer_windows.py # usb_writer_windows.py (Refactoring for Installer Workflow)
import subprocess import subprocess
import os import os
import time import time
import shutil import shutil
import re # For parsing diskpart output import re
import glob # For _find_gibmacos_asset
import traceback
import sys # For checking psutil import import sys # For checking psutil import
# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test # Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test
try: try:
from PyQt6.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox # For user guidance
except ImportError: except ImportError:
class QMessageBox: # Mock for standalone testing class QMessageBox: # Mock for standalone testing
@staticmethod @staticmethod
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'") def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'")
@staticmethod @staticmethod
def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox # Mock button press def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox
Yes = 1 # Mock value Yes = 1 # Mock value
No = 0 # Mock value No = 0 # Mock value
Cancel = 0 # Mock value Cancel = 0 # Mock value
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.")
OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer")
class USBWriterWindows: class USBWriterWindows:
def __init__(self, device_id: str, opencore_qcow2_path: str, macos_qcow2_path: str, def __init__(self, device_id_str: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): progress_callback=None, enhance_plist_enabled: bool = False,
# device_id is expected to be the disk number string, e.g., "1", "2" or "disk 1", "disk 2" target_macos_version: str = ""):
self.disk_number = "".join(filter(str.isdigit, device_id)) # device_id_str is expected to be the disk number string from user, e.g., "1", "2"
self.disk_number = "".join(filter(str.isdigit, device_id_str))
if not self.disk_number: if not self.disk_number:
raise ValueError(f"Invalid device_id format: '{device_id}'. Must contain a disk number.") raise ValueError(f"Invalid device_id format: '{device_id_str}'. Must contain a disk number.")
self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}" self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}"
self.opencore_qcow2_path = opencore_qcow2_path self.macos_download_path = macos_download_path
self.macos_qcow2_path = macos_qcow2_path
self.progress_callback = progress_callback self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled # Not used in Windows writer yet self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version # Not used in Windows writer yet self.target_macos_version = target_macos_version
pid = os.getpid() pid = os.getpid()
self.opencore_raw_path = f"opencore_temp_{pid}.raw" self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
self.macos_raw_path = f"macos_main_temp_{pid}.raw" self.temp_efi_build_dir = f"temp_efi_build_{pid}"
self.temp_efi_extract_dir = f"temp_efi_files_{pid}" self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For 7z extractions
self.temp_files_to_clean = [self.opencore_raw_path, self.macos_raw_path]
self.temp_dirs_to_clean = [self.temp_efi_extract_dir] 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.assigned_efi_letter = None self.assigned_efi_letter = None
def _report_progress(self, message: str): def _report_progress(self, message: str):
@ -66,36 +76,26 @@ class USBWriterWindows:
def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None: def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None:
script_file_path = f"diskpart_script_{os.getpid()}.txt" script_file_path = f"diskpart_script_{os.getpid()}.txt"; output_text = ""
with open(script_file_path, "w") as f: f.write(script_content) with open(script_file_path, "w") as f: f.write(script_content)
output_text = "" # Initialize to empty string
try: try:
self._report_progress(f"Running diskpart script:\n{script_content}") self._report_progress(f"Running diskpart script:\n{script_content}")
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False) process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False)
output_text = (process.stdout or "") + "\n" + (process.stderr or "") # Combine, as diskpart output can be inconsistent output_text = (process.stdout or "") + "\n" + (process.stderr or "")
# Check for known success messages, otherwise assume potential issue or log output for manual check. success_indicators = ["DiskPart successfully", "successfully completed", "succeeded in creating", "successfully formatted", "successfully assigned"]
# This is not a perfect error check for diskpart.
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_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 has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text
if has_error_indicator: if has_error_indicator:
self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}") self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}")
# Optionally raise an error here if script is critical elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text :
# raise subprocess.CalledProcessError(1, "diskpart", output=output_text)
elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text: # Allow benign message
self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}") self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}")
if capture_output_for_parse: return output_text
if capture_output_for_parse:
return output_text
finally: finally:
if os.path.exists(script_file_path): os.remove(script_file_path) if os.path.exists(script_file_path): os.remove(script_file_path)
return output_text if capture_output_for_parse else None # Return None if not capturing for parse return output_text if capture_output_for_parse else None
def _cleanup_temp_files_and_dirs(self): def _cleanup_temp_files_and_dirs(self):
@ -113,8 +113,7 @@ class USBWriterWindows:
def _find_available_drive_letter(self) -> str | None: def _find_available_drive_letter(self) -> str | None:
import string; used_letters = set() import string; used_letters = set()
try: try:
# Check if psutil was imported by the main application if 'psutil' in sys.modules: # Check if psutil was imported by main app
if 'psutil' in sys.modules:
partitions = sys.modules['psutil'].disk_partitions(all=True) partitions = sys.modules['psutil'].disk_partitions(all=True)
for p in partitions: for p in partitions:
if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:" if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:"
@ -124,119 +123,184 @@ class USBWriterWindows:
for letter in "STUVWXYZGHIJKLMNOPQR": for letter in "STUVWXYZGHIJKLMNOPQR":
if letter not in used_letters and letter > 'D': # Avoid A, B, C, D if letter not in used_letters and letter > 'D': # Avoid A, B, C, D
# Further check if letter is truly available (e.g. subst) - more complex, skip for now
return letter return letter
return None return None
def check_dependencies(self): def check_dependencies(self):
self._report_progress("Checking dependencies (qemu-img, diskpart, robocopy)... DD for Win & 7z are manual checks.") self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...")
dependencies = ["qemu-img", "diskpart", "robocopy"]; missing = [dep for dep in dependencies if not shutil.which(dep)] dependencies = ["diskpart", "robocopy", "7z"]
if missing: raise RuntimeError(f"Missing dependencies: {', '.join(missing)}. qemu-img needs install & PATH.") missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
self._report_progress("Base dependencies found. Ensure 'dd for Windows' and '7z.exe' are in PATH if needed.") 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.")
return True 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)
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}.")
return None
def _get_gibmacos_product_folder(self) -> str | None:
from constants import MACOS_VERSIONS # Import for this method
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)
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
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}")
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", 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:
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]
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)
def _create_minimal_efi_template(self, efi_dir_path):
self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}")
oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True)
for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True)
with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("")
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("")
basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}}
try:
with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML)
self._report_progress("Created basic placeholder config.plist.")
except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}")
def format_and_write(self) -> bool: def format_and_write(self) -> bool:
try: try:
self.check_dependencies() self.check_dependencies()
self._cleanup_temp_files_and_dirs() # Clean before start self._cleanup_temp_files_and_dirs()
os.makedirs(self.temp_efi_extract_dir, exist_ok=True) os.makedirs(self.temp_efi_build_dir, exist_ok=True)
self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!") self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!")
self.assigned_efi_letter = self._find_available_drive_letter() self.assigned_efi_letter = self._find_available_drive_letter()
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.") if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
self._report_progress(f"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.") self._report_progress(f"Will assign letter {self.assigned_efi_letter}: to EFI partition.")
diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n" diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n"
diskpart_script_part1 += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\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 += "create partition primary label=macOS_USB\nexit\n" diskpart_script_part1 += f"create partition primary label=\"Install macOS {self.target_macos_version}\" id=AF00\nexit\n" # Set HFS+ type ID
self._run_diskpart_script(diskpart_script_part1) self._run_diskpart_script(diskpart_script_part1)
time.sleep(5) time.sleep(5)
macos_partition_offset_str = "Offset not determined" macos_partition_offset_str = "Offset not determined by diskpart"
macos_partition_number_str = "2 (assumed)" macos_partition_number_str = "2 (assumed)"
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n" diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True) detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
if detail_output: if detail_output:
self._report_progress(f"Detail Partition Output:\n{detail_output}") 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) 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)" if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
# Try to find the line "Partition X" where X is the number we want 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
part_num_search = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE) if part_num_match:
if part_num_search: macos_partition_number_str = part_num_match.group(1)
macos_partition_number_str = part_num_search.group(1)
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}") self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
else: # Fallback if the above specific regex fails
# Look for lines like "Partition 2", "Type : xxxxx"
# This is brittle if diskpart output format changes
partition_lines = [line for line in detail_output.splitlines() if "Partition " in line and "Type :" in line]
if len(partition_lines) > 0 : # Assuming the one we want is the last "Partition X" before other details
last_part_match = re.search(r"Partition\s*(\d+)", partition_lines[-1])
if last_part_match: macos_partition_number_str = last_part_match.group(1)
# --- 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)
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)
shutil.copytree(OC_TEMPLATE_DIR, self.temp_efi_build_dir, dirs_exist_ok=True)
self._report_progress(f"Converting OpenCore QCOW2 to RAW: {self.opencore_raw_path}") temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path]) if not os.path.exists(temp_config_plist_path):
template_plist_src = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") # Name used in prior step
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
if shutil.which("7z"): if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._report_progress("Attempting EFI extraction using 7-Zip...") self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only for this feature)...")
self._run_command(["7z", "x", self.opencore_raw_path, f"-o{self.temp_efi_extract_dir}", "EFI", "-r", "-y"], check=False) if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
source_efi_folder = os.path.join(self.temp_efi_extract_dir, "EFI") else: self._report_progress("config.plist enhancement call failed or had issues.")
if not os.path.isdir(source_efi_folder):
if os.path.exists(os.path.join(self.temp_efi_extract_dir, "BOOTX64.EFI")): source_efi_folder = self.temp_efi_extract_dir
else: raise RuntimeError("Could not extract EFI folder using 7-Zip from OpenCore image.")
target_efi_on_usb = f"{self.assigned_efi_letter}:\\EFI" target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\"
if not os.path.exists(f"{self.assigned_efi_letter}:\\"): # Check if drive letter is mounted if not os.path.exists(target_efi_on_usb_root): # Wait and check again
time.sleep(3) # Wait a bit more time.sleep(3)
if not os.path.exists(f"{self.assigned_efi_letter}:\\"): if not os.path.exists(target_efi_on_usb_root):
# Attempt to re-assign just in case raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign.")
self._report_progress(f"Re-assigning drive letter {self.assigned_efi_letter} to EFI partition...")
reassign_script = f"select disk {self.disk_number}\nselect partition 1\nassign letter={self.assigned_efi_letter}\nexit\n"
self._run_diskpart_script(reassign_script)
time.sleep(3)
if not os.path.exists(f"{self.assigned_efi_letter}:\\"):
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign/re-assign.")
if not os.path.exists(target_efi_on_usb): os.makedirs(target_efi_on_usb, exist_ok=True) self._report_progress(f"Copying final EFI folder to USB ESP ({target_efi_on_usb_root})...")
self._report_progress(f"Copying EFI files from '{source_efi_folder}' to '{target_efi_on_usb}'") 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._run_command(["robocopy", source_efi_folder, target_efi_on_usb, "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True) # Added /XO to exclude older self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}")
else: raise RuntimeError("7-Zip CLI (7z.exe) not found in PATH for EFI extraction.")
self._report_progress(f"Converting macOS QCOW2 to RAW: {self.macos_raw_path}") # --- Prepare BaseSystem ---
self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path]) self._report_progress("Locating BaseSystem image 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.")
abs_macos_raw_path = os.path.abspath(self.macos_raw_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.")
abs_hfs_path = os.path.abspath(self.temp_basesystem_hfs_path)
guidance_message = ( guidance_message = (
f"RAW macOS image conversion complete:\n'{abs_macos_raw_path}'\n\n" f"EFI setup complete on drive {self.assigned_efi_letter}:.\n"
f"Target USB: Disk {self.disk_number} (Path: {self.physical_drive_path})\n" f"BaseSystem HFS image extracted to: '{abs_hfs_path}'.\n\n"
f"The target macOS partition is: Partition {macos_partition_number_str}\n" f"MANUAL STEP REQUIRED FOR MAIN macOS PARTITION:\n"
f"Calculated Offset (approx): {macos_partition_offset_str}\n\n" f"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
"MANUAL STEP REQUIRED using a 'dd for Windows' utility:\n" f"2. Use a 'dd for Windows' utility to write the extracted HFS image.\n"
"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n" f" Target: Disk {self.disk_number} (Path: {self.physical_drive_path}), Partition {macos_partition_number_str} (Offset: {macos_partition_offset_str}).\n"
"2. Carefully identify your 'dd for Windows' utility and its exact syntax.\n" f" Example command (VERIFY SYNTAX FOR YOUR DD TOOL!):\n"
" Common utilities: dd from SUSE (recommended), dd by chrysocome.net.\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"
"3. Example 'dd' command (SYNTAX VARIES GREATLY BETWEEN DD TOOLS!):\n" f" OR, if writing to the whole disk by offset (VERY ADVANCED & RISKY if offset is wrong):\n"
f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} bs=4M --progress`\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"
" (This example writes to the whole disk, which might be okay if your macOS partition is the first primary after EFI and occupies the rest). \n" "3. After writing BaseSystem, manually copy other installer files (like InstallAssistant.pkg or contents of SharedSupport.dmg) from "
" A SAFER (but more complex) approach if your 'dd' supports it, is to write directly to the partition's OFFSET (requires dd that handles PhysicalDrive offsets correctly):\n" 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"
f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} seek=<PARTITION_OFFSET_IN_BLOCKS_OR_BYTES> bs=<YOUR_BLOCK_SIZE> ...`\n" "This tool CANNOT fully automate HFS+ partition writing or HFS+ file copying on Windows."
" (The 'seek' parameter and its units depend on your dd tool. The offset from diskpart is in bytes.)\n\n"
"VERIFY YOUR DD COMMAND AND TARGETS BEFORE EXECUTION. DATA LOSS IS LIKELY IF INCORRECT.\n"
"This tool cannot automate this step due to the variability and risks of 'dd' utilities on Windows."
) )
self._report_progress(f"GUIDANCE:\n{guidance_message}") self._report_progress(f"GUIDANCE:\n{guidance_message}")
QMessageBox.information(None, "Manual macOS Image Write Required", guidance_message) QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message) # Ensure QMessageBox is available or mocked
self._report_progress("Windows USB writing (EFI part automated, macOS part manual guidance provided) process initiated.") self._report_progress("Windows USB installer preparation (EFI automated, macOS content manual guidance provided) initiated.")
return True return True
except Exception as e: except Exception as e:
self._report_progress(f"Error during Windows USB writing: {e}") self._report_progress(f"Error during Windows USB writing: {e}"); self._report_progress(traceback.format_exc())
import traceback; self._report_progress(traceback.format_exc())
return False return False
finally: finally:
if self.assigned_efi_letter: if self.assigned_efi_letter:
@ -244,26 +308,28 @@ class USBWriterWindows:
self._cleanup_temp_files_and_dirs() self._cleanup_temp_files_and_dirs()
if __name__ == '__main__': if __name__ == '__main__':
if platform.system() != "Windows": import traceback
print("This script is for Windows standalone testing."); exit(1) from constants import MACOS_VERSIONS # Needed for _get_gibmacos_product_folder
print("USB Writer Windows Standalone Test - Improved Guidance") if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1)
mock_oc = "mock_oc_win.qcow2"; mock_mac = "mock_mac_win.qcow2" print("USB Writer Windows Standalone Test - Installer Method Guidance")
# Ensure qemu-img is available for mock file creation mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
if not shutil.which("qemu-img"): target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
print("qemu-img not found, cannot create mock files for test. Exiting.") mock_product_name = f"000-00000 - macOS {target_version_cli} 14.x.x"
exit(1) mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
if not os.path.exists(mock_oc): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_oc, "384M"]) os.makedirs(os.path.join(mock_product_folder, "SharedSupport"), exist_ok=True)
if not os.path.exists(mock_mac): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_mac, "1G"]) with open(os.path.join(mock_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg")
disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). THIS DISK WILL BE WIPES: ") 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)
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) if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1)
if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes': if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes':
# USBWriterWindows expects just the disk number string (e.g., "1") writer = USBWriterWindows(disk_id_input, mock_download_dir, print, True, target_version_cli)
writer = USBWriterWindows(disk_id_input, mock_oc, mock_mac, print)
writer.format_and_write() writer.format_and_write()
else: print("Cancelled.") else: print("Cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True)
if os.path.exists(mock_oc): os.remove(mock_oc) # shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template
if os.path.exists(mock_mac): os.remove(mock_mac) print("Mock download dir cleaned up.")
print("Mocks cleaned.")