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>
<key>ACPI</key>
<dict>
<key>Add</key> <array/>
<key>Delete</key> <array/>
<key>Patch</key> <array/>
<key>Add</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>
<dict>
<key>FadtEnableReset</key> <false/>
<key>NormalizeHeaders</key> <false/>
<key>RebaseRegions</key> <false/>
<key>ResetHwSig</key> <false/>
<key>ResetLogoStatus</key> <true/>
<key>SyncTableIds</key> <false/>
<key>FadtEnableReset</key><false/>
<key>NormalizeHeaders</key><false/>
<key>RebaseRegions</key><false/>
<key>ResetHwSig</key><false/>
<key>ResetLogoStatus</key><true/>
<key>SyncTableIds</key><false/>
</dict>
</dict>
<key>Booter</key>
<dict>
<key>MmioWhitelist</key> <array/>
<key>Patch</key> <array/>
<key>MmioWhitelist</key><array/>
<key>Patch</key><array/>
<key>Quirks</key>
<dict>
<key>AllowRelocationBlock</key> <false/>
<key>AvoidRuntimeDefrag</key> <true/>
<key>DevirtualiseMmio</key> <false/> <!-- Change to true for Alder Lake B660/Z690 if needed -->
<key>DisableSingleUser</key> <false/>
<key>DisableVariableWrite</key> <false/>
<key>DiscardHibernateMap</key> <false/>
<key>EnableSafeModeSlide</key> <true/>
<key>EnableWriteUnprotector</key> <false/> <!-- Keep false, OpenRuntime handles this -->
<key>ForceBooterSignature</key> <false/>
<key>ForceExitBootServices</key> <false/>
<key>ProtectMemoryRegions</key> <false/>
<key>ProtectSecureBoot</key> <false/>
<key>ProtectUefiServices</key> <false/>
<key>ProvideCustomSlide</key> <true/>
<key>ProvideMaxSlide</key> <integer>0</integer>
<key>RebuildAppleMemoryMap</key> <false/> <!-- Change to true for Alder Lake if needed -->
<key>ResizeAppleGpuBars</key> <integer>-1</integer>
<key>SetupVirtualMap</key> <true/>
<key>SignalAppleOS</key> <false/>
<key>SyncRuntimePermissions</key> <false/> <!-- Change to true for Alder Lake if needed -->
<key>AllowRelocationBlock</key><false/>
<key>AvoidRuntimeDefrag</key><true/>
<key>DevirtualiseMmio</key><true/><!-- Alder Lake: True -->
<key>DisableSingleUser</key><false/>
<key>DisableVariableWrite</key><false/>
<key>DiscardHibernateMap</key><false/>
<key>EnableSafeModeSlide</key><true/>
<key>EnableWriteUnprotector</key><false/>
<key>ForceBooterSignature</key><false/>
<key>ForceExitBootServices</key><false/>
<key>ProtectMemoryRegions</key><false/>
<key>ProtectSecureBoot</key><false/>
<key>ProtectUefiServices</key><true/><!-- Alder Lake: True -->
<key>ProvideCustomSlide</key><true/>
<key>ProvideMaxSlide</key><integer>0</integer>
<key>RebuildAppleMemoryMap</key><true/><!-- Alder Lake: True -->
<key>ResizeAppleGpuBars</key><integer>-1</integer>
<key>SetupVirtualMap</key><true/>
<key>SignalAppleOS</key><false/>
<key>SyncRuntimePermissions</key><true/><!-- Alder Lake: True -->
</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>
<dict>
<key>Add</key> <array>
<!-- Lilu -->
<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>
<!-- VirtualSMC -->
<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>
<!-- WhateverGreen -->
<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>
<!-- AppleALC -->
<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) -->
<key>Add</key>
<array>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>Lilu.kext</string><key>Comment</key><string>Lilu</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/Lilu</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>VirtualSMC.kext</string><key>Comment</key><string>VirtualSMC</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/VirtualSMC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>SMCProcessor.kext</string><key>Comment</key><string>SMCProcessor for CPU temp</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/SMCProcessor</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>SMCSuperIO.kext</string><key>Comment</key><string>SMCSuperIO for fan speeds</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/SMCSuperIO</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>WhateverGreen.kext</string><key>Comment</key><string>WhateverGreen for Graphics</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/WhateverGreen</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>AppleALC.kext</string><key>Comment</key><string>AppleALC for Audio</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/AppleALC</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>IntelMausi.kext</string><key>Comment</key><string>Intel Ethernet</string><key>Enabled</key><false/><key>ExecutablePath</key><string>Contents/MacOS/IntelMausi</string><key>MaxKernel</key><string></string><key>MinKernel</key><string></string><key>PlistPath</key><string>Contents/Info.plist</string></dict>
<dict><key>Arch</key><string>Any</string><key>BundlePath</key><string>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>
<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>
<dict>
<key>AppleCpuPmCfgLock</key> <false/> <key>AppleXcpmCfgLock</key> <true/> <key>AppleXcpmExtraMsrs</key> <false/>
<key>AppleXcpmForceBoost</key> <false/> <key>CustomPciSerialDevice</key> <false/> <key>CustomSMBIOSGuid</key> <false/>
<key>DisableIoMapper</key> <true/> <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> <false/> <key>SetApfsTrimTimeout</key> <integer>-1</integer>
<key>ThirdPartyDrives</key> <false/> <key>XhciPortLimit</key> <false/>
<key>AppleCpuPmCfgLock</key><false/>
<key>AppleXcpmCfgLock</key><true/><!-- Often true for modern Intel -->
<key>AppleXcpmExtraMsrs</key><false/>
<key>AppleXcpmForceBoost</key><false/>
<key>CustomPciSerialDevice</key><false/>
<key>CustomSMBIOSGuid</key><false/>
<key>DisableIoMapper</key><true/>
<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>
<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>
<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>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>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>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>Misc</key><dict><key>BlessOverride</key><array/><key>Boot</key><dict><key>ConsoleAttributes</key><integer>0</integer><key>HibernateMode</key><string>None</string><key>HibernateSkipsPicker</key><true/><key>HideAuxiliary</key><true/><key>LauncherOption</key><string>Disabled</string><key>LauncherPath</key><string>Default</string><key>PickerAttributes</key><integer>17</integer><key>PickerAudioAssist</key><false/><key>PickerMode</key><string>External</string><key>PickerVariant</key><string>Acidanthera\GoldenGate</string><key>PollAppleHotKeys</key><true/><key>ShowPicker</key><true/><key>TakeoffDelay</key><integer>0</integer><key>Timeout</key><integer>5</integer></dict><key>Debug</key><dict><key>AppleDebug</key><false/><key>ApplePanic</key><false/><key>DisableWatchDog</key><true/><key>DisplayDelay</key><integer>0</integer><key>DisplayLevel</key><integer>2147483650</integer><key>LogModules</key><string>*</string><key>SysReport</key><false/><key>Target</key><integer>0</integer></dict><key>Entries</key><array/><key>Security</key><dict><key>AllowSetDefault</key><true/><key>ApECID</key><integer>0</integer><key>AuthRestart</key><false/><key>BlacklistAppleUpdate</key><true/><key>DmgLoading</key><string>Signed</string><key>EnablePassword</key><false/><key>ExposeSensitiveData</key><integer>6</integer><key>HaltLevel</key><integer>2147483648</integer><key>PasswordHash</key><data></data><key>PasswordSalt</key><data></data><key>ScanPolicy</key><integer>0</integer><key>SecureBootModel</key><string>Disabled</string><key>Vault</key><string>Optional</string></dict><key>Serial</key><dict><key>Init</key><false/><key>Override</key><false/></dict><key>Tools</key><array/></dict>
<key>NVRAM</key><dict><key>Add</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><dict><key>DefaultBackgroundColor</key><data>AAAAAA==</data><key>UIScale</key><data>AQ==</data></dict><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><dict><key>SystemAudioVolume</key><data>Rg==</data><key>boot-args</key><string>-v keepsyms=1 debug=0x100 alcid=1</string><key>csr-active-config</key><data>AAAAAA==</data><key>prev-lang:kbd</key><data>ZW4tVVM6MA==</data><key>run-efi-updater</key><string>No</string></dict></dict><key>Delete</key><dict><key>4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14</key><array><string>UIScale</string><string>DefaultBackgroundColor</string></array><key>7C436110-AB2A-4BBB-A880-FE41995C9F82</key><array><string>boot-args</string><string>csr-active-config</string></array></dict><key>LegacyOverwrite</key><false/><key>LegacySchema</key><dict/><key>WriteFlash</key><true/></dict>
<key>PlatformInfo</key><dict><key>Automatic</key><true/><key>CustomMemory</key><false/><key>Generic</key><dict><key>AdviseFeatures</key><false/><key>MLB</key><string>CHANGE_ME_MLB</string><key>MaxBIOSVersion</key><false/><key>ProcessorType</key><integer>0</integer><key>ROM</key><data>AAAAAA==</data><key>SpoofVendor</key><true/><key>SystemMemoryStatus</key><string>Auto</string><key>SystemProductName</key><string>iMacPro1,1</string><key>SystemSerialNumber</key><string>CHANGE_ME_SERIAL</string><key>SystemUUID</key><string>CHANGE_ME_UUID</string></dict><key>UpdateDataHub</key><true/><key>UpdateNVRAM</key><true/><key>UpdateSMBIOS</key><true/><key>UpdateSMBIOSMode</key><string>Create</string><key>UseRawUuidEncoding</key><false/></dict>
<key>UEFI</key><dict><key>APFS</key><dict><key>EnableJumpstart</key><true/><key>GlobalConnect</key><false/><key>HideVerbose</key><true/><key>JumpstartHotPlug</key><false/><key>MinDate</key><integer>0</integer><key>MinVersion</key><integer>0</integer></dict><key>AppleInput</key><dict><key>AppleEvent</key><string>Builtin</string><key>CustomDelays</key><false/><key>GraphicsInputMirroring</key><true/><key>KeyInitialDelay</key><integer>50</integer><key>KeySubsequentDelay</key><integer>5</integer><key>PointerSpeedDiv</key><integer>1</integer><key>PointerSpeedMul</key><integer>1</integer></dict><key>Audio</key><dict><key>AudioCodec</key><integer>0</integer><key>AudioDevice</key><string></string><key>AudioOutMask</key><integer>-1</integer><key>AudioSupport</key><false/><key>DisconnectHda</key><false/><key>MaximumGain</key><integer>-15</integer><key>MinimumAssistGain</key><integer>-30</integer><key>MinimumAudibleGain</key><integer>-55</integer><key>PlayChime</key><string>Auto</string><key>ResetTrafficClass</key><false/><key>SetupDelay</key><integer>0</integer></dict><key>ConnectDrivers</key><true/><key>Drivers</key><array><string>HfsPlus.efi</string><string>OpenRuntime.efi</string><string>OpenCanopy.efi</string><!-- Add OpenPartitionDxe.efi for some systems --></array><key>Input</key><dict><key>KeyFiltering</key><false/><key>KeyForgetThreshold</key><integer>5</integer><key>KeySupport</key><true/><key>KeySupportMode</key><string>Auto</string><key>KeySwap</key><false/><key>PointerSupport</key><false/><key>PointerSupportMode</key><string>ASUS</string><key>TimerResolution</key><integer>50000</integer></dict><key>Output</key><dict><key>ClearScreenOnModeSwitch</key><false/><key>ConsoleMode</key><string></string><key>DirectGopRendering</key><false/><key>ForceResolution</key><false/><key>GopPassThrough</key><string>Disabled</string><key>IgnoreTextInGraphics</key><false/><key>ProvideConsoleGop</key><true/><key>ReconnectGraphicsOnConnect</key><false/><key>ReconnectOnResChange</key><false/><key>ReplaceTabWithSpace</key><false/><key>Resolution</key><string>Max</string><key>SanitiseClearScreen</key><false/><key>TextRenderer</key><string>BuiltinGraphics</string><key>UIScale</key><integer>-1</integer><key>UgaPassThrough</key><false/></dict><key>ProtocolOverrides</key><dict/><key>Quirks</key><dict><key>ActivateHpetSupport</key><false/><key>DisableSecurityPolicy</key><false/><key>EnableVectorAcceleration</key><true/><key>EnableVmx</key><false/><key>ExitBootServicesDelay</key><integer>0</integer><key>ForceOcWriteFlash</key><false/><key>ForgeUefiSupport</key><false/><key>IgnoreInvalidFlexRatio</key><false/><key>ReleaseUsbOwnership</key><true/><!-- Often True for modern systems --> <key>ReloadOptionRoms</key><false/><key>RequestBootVarRouting</key><true/><key>ResizeGpuBars</key><integer>-1</integer><key>TscSyncTimeout</key><integer>0</integer><key>UnblockFsConnect</key><false/></dict><key>ReservedMemory</key><array/></dict>
</dict>
</plist>

188
README.md
View File

@ -1,130 +1,130 @@
# Skyscope macOS on PC USB Creator Tool
**Version:** 0.8.1 (Alpha)
**Version:** 1.0.0 (Dev - New Workflow)
**Developer:** Miss Casey Jay Topojani
**Business:** Skyscope Sentinel Intelligence
## Vision: Your Effortless Bridge to macOS on PC
Welcome to the Skyscope macOS on PC USB Creator Tool! Our vision is to provide an exceptionally user-friendly, GUI-driven application that fully automates the complex process of creating a bootable macOS USB 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.
* **macOS Version Selection:** Easily choose from popular macOS versions (Sonoma, Ventura, Monterey, Big Sur, Catalina).
* **Automated Docker-OSX Orchestration:**
* **Intelligent Image Pulling:** Automatically pulls the required `sickcodes/docker-osx` image from Docker Hub, with progress displayed.
* **VM Creation & macOS Installation:** Launches the Docker-OSX container where you can interactively install macOS within a QEMU virtual machine.
* **Log Streaming:** View Docker and QEMU logs directly in the application for transparency.
* **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`).
* **Container Management:** Stop and remove the Docker-OSX container after use.
* **Cross-Platform USB Drive Preparation:**
* **USB Detection:** Identifies potential USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows).
* **Automated EFI & macOS System Write (Linux & macOS):**
* Partitions the USB drive with a GUID Partition Table (GPT).
* Creates and formats an EFI System Partition (FAT32) and a main macOS partition (HFS+).
* 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.
* **Windows USB Writing (Partial Automation):**
* Automates EFI partition creation and EFI file copying.
* **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.
* **Experimental `config.plist` Auto-Enhancement:**
* **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).
* **Targeted Modifications:** Optionally attempts to modify the `config.plist` (from the generated `OpenCore.qcow2`) to:
* Add common `DeviceProperties` for Intel iGPUs.
* Set appropriate audio `layout-id`s.
* Ensure necessary Ethernet kexts are enabled.
* 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.
* **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.
* **Intuitive Graphical User Interface (PyQt6):**
* Dark-themed by default (planned).
* Rounded window design (platform permitting).
* Clear, step-by-step workflow.
* Enhanced progress indicators (filling bars, spinners, percentage updates - planned).
* **Automated macOS Installer Acquisition:**
* Directly downloads official macOS installer assets from Apple's servers using `gibMacOS` principles.
* Supports user selection of macOS versions (aiming for Sequoia, Sonoma, Ventura, Monterey, Big Sur, etc.).
* **Automated USB Installer Creation:**
* **Cross-Platform USB Detection:** Identifies suitable USB drives on Linux, macOS, and Windows (using WMI for more accurate detection on Windows).
* **Automated Partitioning:** Creates GUID Partition Table (GPT), an EFI System Partition (FAT32, ~300-550MB), and a main macOS Installer partition (HFS+).
* **macOS Installer Layout:** Automatically extracts and lays out downloaded macOS assets (BaseSystem, installer packages, etc.) onto the USB to create a bootable macOS installer volume.
* **Intelligent OpenCore EFI Setup:**
* Assembles a complete OpenCore EFI folder on the USB's EFI partition.
* Includes essential drivers, kexts, and ACPI SSDTs for broad compatibility.
* **Experimental `config.plist` Auto-Enhancement:**
* If enabled by the user (and running the tool on a Linux host for hardware detection):
* Gathers host hardware information (iGPU, dGPU, Audio, Ethernet, CPU).
* Applies targeted modifications to the `config.plist` to improve compatibility (e.g., Intel iGPU `DeviceProperties`, audio `layout-id`s, enabling Ethernet kexts).
* Specific handling for NVIDIA GPUs (e.g., GTX 970) based on target macOS version to allow booting (e.g., `nv_disable=1` for newer macOS if iGPU is primary, or boot-args for OCLP compatibility).
* Creates a backup of the original `config.plist` before modification.
* **Privilege Handling:** Checks for and advises on necessary admin/root privileges for USB writing.
* **User Guidance:** Provides clear instructions and warnings throughout the process.
## NVIDIA GPU Support Strategy (e.g., GTX 970 on newer macOS)
* **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
* **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.
* **`config.plist` Enhancement is Experimental:**
* 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.
* The applied patches are based on common configurations and may not be optimal or work for all hardware. Always test thoroughly.
* **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.
* **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.
* **Workflow Transition:** The project is currently transitioning from a Docker-OSX based method to a `gibMacOS`-based installer creation method. Not all platform-specific USB writers are fully refactored for this new approach yet.
* **Windows USB Writing:** Creating the HFS+ macOS installer partition and copying files to it from Windows is complex without native HFS+ write support. The EFI part is automated; the main partition might initially require manual steps or use of `dd` for BaseSystem, with file copying being a challenge.
* **`config.plist` Enhancement is Experimental:** Hardware detection for this feature is currently Linux-host only. The range of hardware automatically configured is limited to common setups.
* **Universal Compatibility:** Hackintoshing is inherently hardware-dependent. While this tool aims for broad compatibility, success on every PC configuration cannot be guaranteed.
* **Dependency on External Projects:** Relies on OpenCore and various community-sourced kexts and configurations. The `gibMacOS.py` script (or its underlying principles) is key for downloading assets.
## Prerequisites
1. **Docker:** Must be installed and running. Your user account needs permission to manage Docker.
* [Install Docker Engine](https://docs.docker.com/engine/install/)
2. **Python:** Version 3.8 or newer.
3. **Python Libraries:** Install with `pip install PyQt6 psutil`.
1. **Python:** Version 3.8 or newer.
2. **Python Libraries:** `PyQt6`, `psutil`. Install via `pip install PyQt6 psutil`.
3. **Core Utilities (all platforms, must be in PATH):**
* `git` (used by `gibMacOS.py` and potentially for cloning other resources).
* `7z` or `7za` (7-Zip command-line tool for archive extraction).
4. **Platform-Specific CLI Tools for USB Writing:**
* **Linux (including Debian 13 "Trixie"):**
* `qemu-img` (from `qemu-utils`)
* `parted`
* `kpartx` (from `kpartx` or `multipath-tools`)
* `rsync`
* **Linux (e.g., Debian 13 "Trixie"):**
* `sgdisk`, `parted`, `partprobe` (from `gdisk`, `parted`, `util-linux`)
* `mkfs.vfat` (from `dosfstools`)
* `mkfs.hfsplus` (from `hfsprogs`)
* `rsync`
* `dd` (core utility)
* `apfs-fuse`: Often requires manual compilation (e.g., from `sgan81/apfs-fuse` on GitHub). Typical build dependencies: `git g++ cmake libfuse3-dev libicu-dev zlib1g-dev libbz2-dev libssl-dev`. Ensure it's in your PATH.
* `lsblk`, `partprobe` (from `util-linux`)
* Install most via: `sudo apt update && sudo apt install qemu-utils parted kpartx rsync dosfstools hfsprogs util-linux`
* Install most via: `sudo apt update && sudo apt install gdisk parted dosfstools hfsprogs rsync util-linux p7zip-full` (or `p7zip`)
* **macOS:**
* `qemu-img` (e.g., via Homebrew: `brew install qemu`)
* `diskutil`, `hdiutil`, `rsync` (standard macOS tools).
* `diskutil`, `hdiutil`, `rsync`, `cp`, `bless` (standard system tools).
* `7z` (e.g., via Homebrew: `brew install p7zip`).
* **Windows:**
* `qemu-img` (install and add to PATH).
* `diskpart`, `robocopy` (standard Windows tools).
* `7z.exe` (7-Zip command-line tool, install and add to PATH) - for EFI file extraction.
* 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.
* `diskpart`, `robocopy` (standard system tools).
* `7z.exe` (install and add to PATH).
* A "dd for Windows" utility (user must install and ensure it's in PATH).
## How to Run
## How to Run (Development Phase)
1. Ensure all prerequisites for your operating system are met.
2. Clone this repository or download the source files.
3. Install Python libraries: `pip install PyQt6 psutil`.
4. Execute `python main_app.py`.
5. **Important for USB Writing:**
1. Ensure all prerequisites for your OS are met.
2. Clone this repository.
3. **Crucial:** Clone `corpnewt/gibMacOS` into a `./scripts/gibMacOS/` subdirectory within this project, or ensure `gibMacOS.py` is in the project root or your system PATH and update `GIBMACOS_SCRIPT_PATH` in `main_app.py` if necessary.
4. Install Python libraries: `pip install PyQt6 psutil`.
5. Execute `python main_app.py`.
6. **For USB Writing Operations:**
* **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).
* **Windows:** Run the application as Administrator.
* **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 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".
* Select your desired macOS version from the dropdown menu.
* Click "Create VM and Start macOS Installation".
* The tool will first pull the necessary Docker image (progress 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.
* 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.
2. **Step 2: Extract VM Images**
* 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".
* Select your desired macOS version (e.g., Sequoia, Sonoma).
* Choose a directory on your computer to save the downloaded assets.
* 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.
2. **Step 2: Create Bootable USB Installer**
* Once downloads are complete, connect your target USB flash drive (16GB+ recommended).
* Click "Refresh List" to detect USB drives.
* **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.
* **(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.
* **CRITICAL WARNING:** Double-check your selection. The next action will erase the selected USB drive.
* Click "Write Images to USB Drive". Confirm the data erasure warning.
* The process will now:
* (If enhancement enabled) Attempt to modify the `config.plist` within the source OpenCore image.
* Partition and format your USB drive.
* Copy EFI files to the USB's EFI partition.
* Copy macOS system files to the USB's main partition. (On Windows, this step requires manual `dd` operation as guided by the application).
* This is a lengthy process. Monitor the progress in the output area.
5. **Boot!**
* 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).
* **(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 USB selection. The next action will erase the entire USB drive.
* Click "Create macOS Installer USB". Confirm the data erasure warning.
* The tool will:
* Partition and format the USB drive.
* Extract and write the macOS BaseSystem to make the USB bootable.
* Copy necessary macOS installer packages and files to the USB.
* 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 progress in the output area and status bar.
3. **Boot Your PC from the USB!**
* 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.
* **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).
* 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.
@ -133,7 +133,7 @@ This project is dedicated to creating a seamless experience, from selecting your
## 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

View File

@ -6,8 +6,8 @@ import psutil
import platform
import ctypes
import json
import re
import traceback # For better error logging
import re # For progress parsing
import traceback # For error reporting
import shutil # For shutil.which
from PyQt6.QtWidgets import (
@ -16,7 +16,7 @@ from PyQt6.QtWidgets import (
QFileDialog, QGroupBox, QLineEdit, QProgressBar, QCheckBox
)
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
# 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
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")
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")
class WorkerSignals(QObject):
progress = pyqtSignal(str)
finished = pyqtSignal(str)
finished = pyqtSignal(str) # Can carry a success message or final status
error = pyqtSignal(str)
progress_value = pyqtSignal(int)
# New signal for determinate progress
progress_value = pyqtSignal(int) # Percentage 0-100
class GibMacOSWorker(QObject):
signals = WorkerSignals()
@ -61,16 +66,15 @@ class GibMacOSWorker(QObject):
@pyqtSlot()
def run(self):
try:
script_to_run = ""
if os.path.exists(GIBMACOS_SCRIPT_PATH):
script_to_run = GIBMACOS_SCRIPT_PATH
elif shutil.which("gibMacOS.py"): # Check if it's in PATH
script_to_run = "gibMacOS.py"
elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")): # Check alongside main_app.py
script_to_run = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "gibMacOS.py")
else:
self.signals.error.emit(f"gibMacOS.py not found at expected locations or in PATH.")
if not os.path.exists(script_to_run):
alt_script_path = os.path.join(os.path.dirname(os.path.dirname(GIBMACOS_SCRIPT_PATH)), "gibMacOS.py") # if main_app is in src/
script_to_run = alt_script_path if os.path.exists(alt_script_path) else "gibMacOS.py"
if not os.path.exists(script_to_run) and not shutil.which(script_to_run): # Check if it's in PATH
self.signals.error.emit(f"gibMacOS.py not found at expected locations ({GIBMACOS_SCRIPT_PATH}, {alt_script_path}) or in PATH.")
return
else:
script_to_run = GIBMACOS_SCRIPT_PATH
version_for_gib = MACOS_VERSIONS.get(self.version_key, self.version_key)
os.makedirs(self.download_path, exist_ok=True)
@ -91,10 +95,15 @@ class GibMacOSWorker(QObject):
break
line_strip = 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:
try: self.signals.progress_value.emit(int(progress_match.group(1)))
except ValueError: pass
try:
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()
return_code = self.process.wait()
@ -108,7 +117,7 @@ class GibMacOSWorker(QObject):
else:
self.signals.error.emit(f"Failed to download macOS '{self.version_key}' (gibMacOS exit code {return_code}). Check logs.")
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:
self.signals.error.emit(f"An error occurred during macOS download: {str(e)}\n{traceback.format_exc()}")
finally:
@ -147,9 +156,6 @@ class USBWriterWorker(QObject):
if writer_cls is None:
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(
device=self.device,
macos_download_path=self.macos_download_path,
@ -158,6 +164,11 @@ class USBWriterWorker(QObject):
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():
self.signals.finished.emit("USB writing process completed successfully.")
else:
@ -170,7 +181,7 @@ class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle(APP_NAME)
self.setGeometry(100, 100, 800, 700) # Adjusted height
self.setGeometry(100, 100, 800, 750)
self.active_worker_thread = None
self.macos_download_path = None
@ -182,7 +193,7 @@ class MainWindow(QMainWindow):
self._setup_ui()
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.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)
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()
self.version_combo.addItems(MACOS_VERSIONS.keys()); selection_layout.addWidget(self.version_label); selection_layout.addWidget(self.version_combo)
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)
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.cancel_operation_button = QPushButton("Cancel Current Operation")
self.cancel_operation_button.clicked.connect(self.stop_current_operation)
self.cancel_operation_button.setEnabled(False)
download_layout.addWidget(self.cancel_operation_button)
download_group.setLayout(download_layout)
main_layout.addWidget(download_group)
self.cancel_operation_button.setEnabled(False); 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")
self.usb_layout = QVBoxLayout()
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.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()
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..."):
# 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)
if busy_status:
self.base_status_message = message
if not self.spinner_timer.isActive(): self.spinner_timer.start(150)
self._update_spinner_status()
self.progress_bar.setRange(0,0)
# Progress bar range set by _start_worker based on provides_progress
else:
self.spinner_timer.stop()
self.status_bar.showMessage(message or "Ready.", 7000)
if not busy_status: # After an operation, always update all button states
self.update_all_button_states()
def _update_spinner_status(self):
if self.spinner_timer.isActive():
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
if self.active_worker_thread and self.active_worker_thread.isRunning():
active_worker_provides_progress = getattr(self.active_worker_thread, "provides_progress", False)
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()}%)")
else:
current_message = f"{self.base_status_message} ({self.progress_bar.value()}%)"
else: # Indeterminate
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)
elif not (self.active_worker_thread and self.active_worker_thread.isRunning()):
self.spinner_timer.stop()
@ -301,14 +323,23 @@ class MainWindow(QMainWindow):
self.current_worker_instance = worker_instance
if provides_progress:
self.progress_bar.setRange(0,100)
self.progress_bar.setRange(0,100); self.progress_bar.setValue(0)
# 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:
self.progress_bar.setRange(0,0)
self.active_worker_thread = QThread(); self.active_worker_thread.setObjectName(worker_name + "_thread")
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.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))
@ -322,18 +353,24 @@ class MainWindow(QMainWindow):
def update_progress_bar_value(self, value):
if self.progress_bar.maximum() == 0: self.progress_bar.setRange(0,100)
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):
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
if specific_finished_slot: specific_finished_slot(message)
self._set_ui_busy(False, final_msg)
def _handle_worker_error(self, error_message, worker_name, specific_error_slot):
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
if specific_error_slot: specific_error_slot(error_message)
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
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)
if not self._start_worker(worker, self.macos_download_finished, self.macos_download_error,
"macos_download",
f"Downloading macOS {selected_version_name} assets...",
provides_progress=True): # Assuming GibMacOSWorker will emit progress_value
"macos_download", # worker_name
f"Downloading macOS {selected_version_name} assets...", # busy_message
provides_progress=True): # GibMacOSWorker now attempts to provide progress
self._set_ui_busy(False, "Failed to start macOS download operation.")
@pyqtSlot(str)
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)
# self.macos_download_path is set. UI update handled by generic handler.
@pyqtSlot(str)
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)
self.macos_download_path = None
# UI reset by generic handler.
def stop_current_operation(self):
if self.current_worker_instance and hasattr(self.current_worker_instance, 'stop'):
self.output_area.append(f"
--- Attempting to stop {self.active_worker_thread.objectName().replace('_thread','')} ---")
worker_name_display = "Operation"
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()
else:
self.output_area.append("
--- No active stoppable operation or stop method not implemented for current worker. ---")
self.output_area.append("\n--- 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):
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
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.output_area.append("
Scanning for disk devices...")
if platform.system() == "Windows":
self.usb_drive_combo.clear(); current_selection_text = getattr(self, '_current_usb_selection_text', None); self.output_area.append("\nScanning for disk devices...")
current_os = platform.system()
self.windows_usb_guidance_label.setVisible(current_os == "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.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"
try:
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:
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
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)
except Exception as e: self.output_area.append(f"Error scanning Windows USBs with PowerShell: {e}"); self.windows_disk_id_input.setVisible(True)
else:
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:
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:
partitions = psutil.disk_partitions(all=False); potential_usbs = []
for p in partitions:
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
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
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 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:
try: usage = psutil.disk_usage(p.mountpoint); size_gb = usage.total / (1024**3)
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.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
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
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 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()
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("
USB write cancelled."); return
# USBWriterWorker now needs different args
# The platform specific writers (USBWriterLinux etc) will need to be updated to accept macos_download_path
# 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",
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)
if not self._start_worker(usb_worker, self.usb_write_finished, self.usb_write_error, "usb_write_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.")
@pyqtSlot(str)
@ -470,7 +525,7 @@ USB write cancelled."); return
@pyqtSlot(str)
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()
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)
@ -483,9 +538,5 @@ USB write cancelled."); return
if __name__ == "__main__":
import traceback # Ensure traceback is available for GibMacOSWorker
import shutil # Ensure shutil is available for GibMacOSWorker path check
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
import traceback; import shutil
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
# --- Mappings ---
# Values are typically byte-swapped for device-id and some ig-platform-id representations in OpenCore
# For AAPL,ig-platform-id, the first two bytes are often the device-id (swapped), last two are platform related.
# Example: UHD 630 (Desktop Coffee Lake) device-id 0x3E9B -> data <9B3E0000>
# ig-platform-id commonly 0x3E9B0007 -> data <07009B3E> (or other variants)
# For AAPL,ig-platform-id, byte order in <Data> can be direct or swapped depending on source.
# OpenCore usually expects direct byte order for data values (e.g. 0A009B46 for 0x469B000A).
# The values below are what should be written as data (hex bytes).
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"},
# 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"},
# 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"},
# Alder Lake-S Desktop (UHD 730/750/770) - device-id often needs to be accurate
"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
"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)
"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)
# Alternative Alder Lake platform-id (often when dGPU is primary)
"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: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"},
"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"},
# Alder Lake-S Desktop iGPUs (e.g., UHD 730, UHD 770)
# For driving a display (Desktop): AAPL,ig-platform-id = 0x469B000A (Data: 0A009B46) or 0x4692000A (Data: 0A009246)
# device-id is often the PCI device ID itself, byte-swapped. e.g., 0x4690 -> <90460000>
"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
"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: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
# Headless mode (if dGPU is primary) for Alder Lake: AAPL,ig-platform-id = 0x04001240 (Data: 04001240)
"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)"
# Primary keys are now Codec Names. PCI IDs are secondary/fallback.
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 ALC255": 11, "Realtek ALC256": 11, "Realtek ALC257": 11,
"Realtek ALC269": 11, "Realtek ALC271": 11, "Realtek ALC282": 11,
@ -54,12 +54,12 @@ AUDIO_LAYOUTS = {
"Realtek ALC295": 11,
"Realtek ALC662": 5, "Realtek ALC671": 11,
"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 ALC1200": 7,
"Realtek ALC1220": 7, "Realtek ALC1220-VB": 7, # VB variant often uses same layouts
"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:a2f0": 1, # Series 200 HD Audio (Kaby Lake)
"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
"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)
"10ec:8168": "RealtekRTL8111.kext", "10ec:8111": "RealtekRTL8111.kext",
"10ec:2502": "LucyRTL8125Ethernet.kext", # Realtek RTL8125 2.5GbE
"10ec:2600": "LucyRTL8125Ethernet.kext", # Realtek RTL8125B 2.5GbE
"8086:15ec": "AppleIntelI210Ethernet.kext", # I225-V (Often needs AppleIGB.kext or specific patches)
"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):
if progress_callback: progress_callback(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()}")
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"
@ -115,23 +113,28 @@ def enhance_config_plist(plist_path: str, target_macos_version_name: str, progre
# 1. Intel iGPU
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)
if intel_igpu_on_host:
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 lookup_key.startswith("8086:46") and dgpu_present: # Basic check for Alder Lake iGPU + dGPU
lookup_key_dgpu = f"{lookup_key}_dgpu"
if lookup_key_dgpu in INTEL_IGPU_DEFAULTS:
lookup_key = lookup_key_dgpu
_report(f"Intel Alder Lake iGPU ({intel_igpu_on_host['description']}) detected with a dGPU. Using dGPU-specific properties if available.")
# If a dGPU is also present, prefer headless iGPU setup if available.
final_lookup_key = lookup_key
if dgpu_present and f"{lookup_key}_headless" in INTEL_IGPU_DEFAULTS:
final_lookup_key = f"{lookup_key}_headless"
_report(f"Intel iGPU ({intel_igpu_on_host['description']}) detected with a dGPU. Applying headless properties: {final_lookup_key}")
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:
_report(f"Applying properties for Intel iGPU: {intel_igpu_on_host['description']} ({lookup_key}).")
if final_lookup_key and final_lookup_key in INTEL_IGPU_DEFAULTS:
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
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
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:
_report(f"Detected audio codecs: {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():
if not known_codec_key.startswith("pci_"): # Ensure we are checking codec names, not PCI IDs
# Simple substring match or more specific regex
# Example: "Realtek ALC255" should match "ALC255" if key is "ALC255"
# Or if key is "Realtek ALC255" it matches directly
# For "Codec: Realtek ALC255" we might want to extract "Realtek ALC255"
# 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, {})
if not known_codec_key.startswith("pci_"):
# Try to match the core part of the codec name
# e.g. "Realtek ALC897" should match a key like "ALC897" or "Realtek ALC897"
if known_codec_key.lower() in codec_name_full.lower():
_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, {})
new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little'))
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
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_layout_set = True; 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.")
for dev in pci_devices:
if dev['type'] == 'Audio':
lookup_key = f"pci_{dev['vendor_id']}:{dev['device_id']}" # PCI ID keys are prefixed
if lookup_key in AUDIO_LAYOUTS:
layout_id = AUDIO_LAYOUTS[lookup_key]
_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, {})
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, {})
new_layout_data = plistlib.Data(layout_id.to_bytes(1, 'little'))
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
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_layout_set = True; break
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
# 4. NVIDIA GTX 970 Specific Adjustments
gtx_970_present = any(dev['vendor_id'] == '10de' and dev['device_id'] == '13c2' for dev in pci_devices)
if gtx_970_present:
nvidia_gtx_970_present = any(dev['vendor_id'] == '10de' and dev['device_id'] == '13c2' for dev in pci_devices)
if nvidia_gtx_970_present:
_report("NVIDIA GTX 970 detected.")
high_sierra_and_older_versions = ["high sierra", "sierra", "el capitan"]
is_high_sierra_or_older_target = target_macos_version_name.lower() in high_sierra_and_older_versions
high_sierra_versions = ["high sierra", "sierra"];
is_legacy_nvidia_target = target_macos_version_name.lower() in high_sierra_versions
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')
_report(" Configured for NVIDIA Web Drivers (High Sierra or older target).")
else: # Mojave and newer
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:
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.")
else:
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
final_boot_args_str = ' '.join(sorted(list(boot_args)))
if boot_args_section.get('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}'")
modified_plist = True
_report(f"Updated boot-args to: '{final_boot_args_str}'"); modified_plist = True
if not modified_plist:
_report("No changes 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 # No error, just no action
_report("No new modifications made to config.plist based on detected hardware or existing settings were different from defaults.")
if platform.system() != "Linux" and not pci_devices : return True
try:
with open(plist_path, 'wb') as f:
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
except Exception as e: # ... (restore backup logic same as before)
try: # Save logic (same as before)
with open(plist_path, 'wb') as f: plistlib.dump(config_data, f, sort_keys=True, fmt=plistlib.PlistFormat.XML)
_report(f"Successfully saved config.plist to {plist_path}"); return True
except Exception as e:
_report(f"Error saving modified plist file {plist_path}: {e}")
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
# 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__':
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"
# 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 = {
"DeviceProperties": {"Add": {}},
"Kernel": {"Add": [
@ -266,27 +252,26 @@ if __name__ == '__main__':
if platform.system() != "Linux":
print("Mocking hardware info for non-Linux.")
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': '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_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 (should enable iGPU, audio [ALC1220 layout 7], ethernet [LucyRTL8125]) ---")
print("\n--- Testing with Sonoma (GTX 970 + iGPU present) ---")
success_sonoma = enhance_config_plist(dummy_plist_path, "Sonoma", print)
print(f"Plist enhancement for Sonoma {'succeeded' if success_sonoma else 'failed'}.")
if success_sonoma:
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 iGPU props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(INTEL_IGPU_PCI_PATH)}")
print(f" Sonoma Audio props: {modified_data.get('DeviceProperties',{}).get('Add',{}).get(AUDIO_PCI_PATH_FALLBACK)}")
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)}") # 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)}") # Should have ALC897 layout
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 "AppleALC.kext" in kext.get("BundlePath",""): print(f" AppleALC.kext Enabled: {kext.get('Enabled')}")
if platform.system() != "Linux":
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 os
import time
import shutil
import glob
import re
import plistlib # For plist_modifier call, and potentially for InstallInfo.plist
import plistlib
try:
from plist_modifier import enhance_config_plist
@ -13,25 +13,23 @@ except ImportError:
enhance_config_plist = None
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")
class USBWriterLinux:
def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""):
target_macos_version: str = ""): # target_macos_version is display name e.g. "Sonoma"
self.device = device
self.macos_download_path = macos_download_path
self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version # String name like "Sonoma"
self.target_macos_version = target_macos_version
pid = os.getpid()
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"temp_efi_build_{pid}"
self.temp_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_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_dirs_to_clean = [
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):
@ -52,9 +50,9 @@ class USBWriterLinux:
process = subprocess.run(
command, check=check, capture_output=capture_output, text=True, timeout=timeout,
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.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
return process
@ -64,7 +62,7 @@ class USBWriterLinux:
def _cleanup_temp_files_and_dirs(self):
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):
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"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. Please install them (e.g., hfsprogs, p7zip-full)."
msg = f"Missing dependencies: {', '.join(missing_deps)}. Please install them (e.g., hfsprogs, p7zip-full, gdisk)."
self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("All critical dependencies for Linux USB installer creation found.")
return True
def _find_source_file(self, patterns: list[str], description: str) -> str | None:
"""Finds the first existing file matching a list of glob patterns within self.macos_download_path."""
self._report_progress(f"Searching for {description} in {self.macos_download_path}...")
for pattern in patterns:
# Using iglob for efficiency if many files, but glob is fine for fewer expected matches
found_files = glob.glob(os.path.join(self.macos_download_path, "**", pattern), recursive=True)
def _get_gibmacos_product_folder(self) -> str:
"""Heuristically finds the main product folder within gibMacOS downloads."""
# gibMacOS often creates .../publicrelease/XXX - macOS [VersionName] [VersionNum]/
# We need to find this folder.
_report = self._report_progress
_report(f"Searching for macOS product folder in {self.macos_download_path} for version {self.target_macos_version}")
version_parts = self.target_macos_version.split(" ") # e.g., "Sonoma" or "Mac OS X", "High Sierra"
primary_name = version_parts[0] # "Sonoma", "Mac", "High"
if primary_name == "Mac" and len(version_parts) > 2 and version_parts[1] == "OS": # "Mac OS X"
primary_name = "OS X"
if len(version_parts) > 2 and version_parts[2] == "X": primary_name = "OS X" # For "Mac OS X"
possible_folders = []
for root, dirs, _ in os.walk(self.macos_download_path):
for d_name in dirs:
# Check if directory name contains "macOS" and a part of the target version name/number
if "macOS" in d_name and (primary_name in d_name or self.target_macos_version in d_name):
possible_folders.append(os.path.join(root, d_name))
if not possible_folders:
_report(f"Could not automatically determine specific product folder. Using base download path: {self.macos_download_path}")
return self.macos_download_path
# Prefer shorter paths or more specific matches if multiple found
# This heuristic might need refinement. For now, take the first plausible one.
_report(f"Found potential product folder(s): {possible_folders}. Using: {possible_folders[0]}")
return possible_folders[0]
def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder: str, description: str) -> str | None:
"""Finds the first existing file matching a list of glob patterns within the product_folder."""
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:
# Prefer files not inside .app bundles if multiple are found, unless it's the app itself.
# This is a simple heuristic.
non_app_files = [f for f in found_files if ".app/" not in f]
target_file = non_app_files[0] if non_app_files else found_files[0]
self._report_progress(f"Found {description} at: {target_file}")
return target_file
self._report_progress(f"Warning: {description} not found with patterns: {patterns}")
# 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
def _extract_hfs_from_dmg(self, dmg_path: str, output_hfs_path: str) -> bool:
"""Extracts the primary HFS+ partition image (e.g., '4.hfs') from a DMG."""
# Assumes BaseSystem.dmg or similar that contains a HFS+ partition image.
temp_extract_dir = f"temp_hfs_extract_{os.getpid()}"
os.makedirs(temp_extract_dir, exist_ok=True)
def _extract_basesystem_hfs_from_source(self, source_dmg_path: str, output_hfs_path: str) -> bool:
"""Extracts the primary HFS+ partition image (e.g., '4.hfs') from a source DMG (BaseSystem.dmg or InstallESD.dmg)."""
os.makedirs(self.temp_dmg_extract_dir, exist_ok=True)
try:
self._report_progress(f"Extracting HFS+ partition image from {dmg_path}...")
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar)
self._run_command(["7z", "e", "-tdmg", dmg_path, "*.hfs", f"-o{temp_extract_dir}"], check=True)
self._report_progress(f"Extracting HFS+ partition image from {source_dmg_path} into {self.temp_dmg_extract_dir}...")
# 7z e -tdmg <dmg_path> *.hfs -o<output_dir_for_hfs> (usually 4.hfs or similar for BaseSystem)
# For InstallESD.dmg, it might be a different internal path or structure.
# Assuming the target is a standard BaseSystem.dmg or a DMG containing such structure.
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(temp_extract_dir, "*.hfs"))
if not hfs_files: raise RuntimeError(f"No .hfs file found after extracting {dmg_path}")
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"))
if not hfs_files:
# Fallback: try extracting * (if only one file inside a simple DMG, like some custom BaseSystem.dmg)
self._run_command(["7z", "e", "-tdmg", source_dmg_path, "*", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*")) # Check all files
hfs_files = [f for f in hfs_files if not f.endswith((".xml", ".chunklist", ".plist")) and os.path.getsize(f) > 100*1024*1024] # Filter out small/meta files
if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {source_dmg_path}")
final_hfs_file = max(hfs_files, key=os.path.getsize) # Assume largest is the one
self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}")
shutil.move(final_hfs_file, output_hfs_path)
shutil.move(final_hfs_file, output_hfs_path) # Use shutil.move for local files
return True
except Exception as e:
self._report_progress(f"Error during HFS extraction from DMG: {e}")
self._report_progress(f"Error during HFS extraction from DMG: {e}\n{traceback.format_exc()}")
return False
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:
try:
@ -139,7 +177,7 @@ class USBWriterLinux:
self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...")
self._run_command(["sudo", "sgdisk", "--zap-all", self.device])
self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device])
self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", "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)
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])
# --- 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")
if not basesystem_dmg_path: raise RuntimeError("Essential macOS installer DMG/PKG not found in download path.")
product_folder = self._get_gibmacos_product_folder()
if basesystem_dmg_path.endswith(".pkg") or "SharedSupport.dmg" in os.path.basename(basesystem_dmg_path) :
# If we found InstallAssistant.pkg or SharedSupport.dmg, we need to extract BaseSystem.hfs from it.
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 installer assets.")
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}")
# Find BaseSystem.dmg (or equivalent like InstallESD.dmg if BaseSystem.dmg is not directly available)
# Some gibMacOS downloads might have InstallESD.dmg which contains BaseSystem.dmg.
# Others might have BaseSystem.dmg directly.
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
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._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._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")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
# Find original BaseSystem.dmg and chunklist in download path to copy them
actual_bs_dmg = self._find_source_file(["BaseSystem.dmg"], "original BaseSystem.dmg for copying")
if actual_bs_dmg:
self._report_progress(f"Copying {actual_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")])
# Copy original BaseSystem.dmg and .chunklist from gibMacOS output
original_bs_dmg = self._find_gibmacos_asset(["BaseSystem.dmg"], product_folder, "original BaseSystem.dmg")
if original_bs_dmg:
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist")
if os.path.exists(original_bs_chunklist):
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
else: self._report_progress("Warning: Original BaseSystem.dmg not found in product folder to copy to CoreServices.")
bs_chunklist = actual_bs_dmg.replace(".dmg", ".chunklist")
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")
install_info_src = self._find_gibmacos_asset(["InstallInfo.plist"], product_folder, "InstallInfo.plist")
if install_info_src:
self._report_progress(f"Copying {install_info_src} to {self.mount_point_usb_macos_target}/InstallInfo.plist")
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")])
else: self._report_progress("Warning: InstallInfo.plist not found in 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)
self._report_progress("Placeholder: Copying macOS installation packages to USB (e.g., /System/Installation/Packages)...")
# Example: sudo rsync -a /path/to/downloaded_packages_dir/ /mnt/usb_macos_target/System/Installation/Packages/
# 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")])
# Copy Packages and other assets
packages_target_path = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_target_path])
# Try to find and copy InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg contents for packages
# This part is complex, as gibMacOS output varies.
# If InstallAssistant.pkg is found, its contents (especially packages) are needed.
# If SharedSupport.dmg is found, its contents are needed.
install_assistant_pkg = self._find_gibmacos_asset(["InstallAssistant.pkg"], product_folder, "InstallAssistant.pkg")
if install_assistant_pkg:
self._report_progress(f"Copying contents of InstallAssistant.pkg (Packages) from {os.path.dirname(install_assistant_pkg)} to {packages_target_path} (simplified, may need selective copy)")
# This is a placeholder. Real logic would extract from PKG or copy specific subfolders/files.
# For now, just copy the PKG itself as an example.
self._run_command(["sudo", "cp", install_assistant_pkg, packages_target_path])
else:
shared_support_dmg = self._find_gibmacos_asset(["SharedSupport.dmg"], product_folder, "SharedSupport.dmg for packages")
if shared_support_dmg:
self._report_progress(f"Copying contents of SharedSupport.dmg from {shared_support_dmg} to {packages_target_path} (simplified)")
# Mount SharedSupport.dmg and rsync contents, or 7z extract and rsync
# Placeholder: copy the DMG itself. Real solution needs extraction.
self._run_command(["sudo", "cp", shared_support_dmg, packages_target_path])
else:
self._report_progress("Warning: Neither InstallAssistant.pkg nor SharedSupport.dmg found for main packages. Installer may be incomplete.")
# --- 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...")
if not os.path.isdir(OC_TEMPLATE_DIR):
self._report_progress(f"FATAL: OpenCore template directory not found at {OC_TEMPLATE_DIR}. Cannot proceed."); return False
if not os.path.isdir(OC_TEMPLATE_DIR): self._report_progress(f"FATAL: OpenCore template dir not found: {OC_TEMPLATE_DIR}"); return False
self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}")
self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) # 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
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")):
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
shutil.move(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path)
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
self._run_command(["sudo", "mv", os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist"), temp_config_plist_path])
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._report_progress("Attempting to enhance config.plist...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress):
self._report_progress("config.plist enhancement successful.")
else: self._report_progress("config.plist enhancement failed or had issues. Continuing with (potentially original template) plist.")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.")
else: self._report_progress("config.plist enhancement failed or had issues.")
self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp])
self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...")
@ -235,41 +283,47 @@ class USBWriterLinux:
return True
except Exception as e:
self._report_progress(f"An error occurred during USB writing: {e}")
import traceback; self._report_progress(traceback.format_exc())
self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}")
return False
finally:
self._cleanup_temp_files_and_dirs()
if __name__ == '__main__':
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()}"
os.makedirs(mock_download_dir, exist_ok=True)
# Create a dummy placeholder for what gibMacOS might download
# This is highly simplified. A real gibMacOS download has a complex structure.
# For this test, we'll simulate having BaseSystem.dmg and InstallInfo.plist
mock_install_data_path = os.path.join(mock_download_dir, "macOS_Install_Data") # Simplified path
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
# Create a more structured mock download similar to gibMacOS output
product_name_slug = f"000-00000 - macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'} 14.0" # Example
specific_product_folder = os.path.join(mock_download_dir, "publicrelease", product_name_slug)
os.makedirs(specific_product_folder, exist_ok=True)
# Mock BaseSystem.dmg (tiny, not functional, for path testing)
dummy_bs_dmg_path = os.path.join(specific_product_folder, "BaseSystem.dmg")
if not os.path.exists(dummy_bs_dmg_path):
# 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
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(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):
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':
writer = USBWriterLinux(
device=test_device,
macos_download_path=mock_download_dir,
macos_download_path=mock_download_dir, # Pass base download dir
progress_callback=print,
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()
else: print("Test cancelled by user.")
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(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(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 os
import time
import shutil # For checking command existence
import plistlib # For parsing diskutil list -plist output
import shutil
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:
def __init__(self, device: str, opencore_qcow2_path: str, macos_qcow2_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): # New args
self.device = device # Should be like /dev/diskX
self.opencore_qcow2_path = opencore_qcow2_path
self.macos_qcow2_path = macos_qcow2_path
def __init__(self, device: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""):
self.device = device # e.g., /dev/diskX
self.macos_download_path = macos_download_path
self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled # Store
self.target_macos_version = target_macos_version # Store
self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version
pid = os.getpid()
self.opencore_raw_path = f"opencore_temp_{pid}.raw"
self.macos_raw_path = f"macos_main_temp_{pid}.raw"
self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}"
self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs" # Use /tmp for macOS
self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}"
self.temp_opencore_mount = f"/tmp/opencore_efi_temp_skyscope_{pid}" # For source BaseSystem.dmg's EFI (if needed)
self.temp_usb_esp_mount = f"/tmp/usb_esp_temp_skyscope_{pid}"
self.temp_macos_source_mount = f"/tmp/macos_source_temp_skyscope_{pid}"
self.temp_macos_source_mount = f"/tmp/macos_source_temp_skyscope_{pid}" # Not used in this flow
self.temp_usb_macos_target_mount = f"/tmp/usb_macos_target_temp_skyscope_{pid}"
self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions
self.temp_files_to_clean = [self.opencore_raw_path, self.macos_raw_path]
self.temp_mount_points_to_clean = [
self.temp_opencore_mount, self.temp_usb_esp_mount,
self.temp_macos_source_mount, self.temp_usb_macos_target_mount
self.temp_files_to_clean = [self.temp_basesystem_hfs_path]
self.temp_dirs_to_clean = [
self.temp_efi_build_dir, self.temp_opencore_mount,
self.temp_usb_esp_mount, self.temp_macos_source_mount,
self.temp_usb_macos_target_mount, self.temp_dmg_extract_dir
]
self.attached_raw_images_devices = [] # Store devices from hdiutil attach
self.attached_dmg_devices = [] # Store devices from hdiutil attach
def _report_progress(self, message: str):
print(message) # For standalone testing
if self.progress_callback:
self.progress_callback(message)
def _report_progress(self, message: str): # ... (same)
if self.progress_callback: self.progress_callback(message)
else: print(message)
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None):
def _run_command(self, command: list[str], check=True, capture_output=False, timeout=None, shell=False): # ... (same)
self._report_progress(f"Executing: {' '.join(command)}")
try:
process = subprocess.run(
command, check=check, capture_output=capture_output, text=True, timeout=timeout
)
process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell)
if capture_output:
if process.stdout and process.stdout.strip():
self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip():
self._report_progress(f"STDERR: {process.stderr.strip()}")
if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}")
if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}")
return process
except subprocess.TimeoutExpired:
self._report_progress(f"Command {' '.join(command)} timed out after {timeout} seconds.")
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
except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise
except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise
except FileNotFoundError: self._report_progress(f"Error: Command '{command[0]}' not found."); raise
def _cleanup_temp_files(self):
self._report_progress("Cleaning up temporary image files...")
def _cleanup_temp_files_and_dirs(self): # Updated for macOS
self._report_progress("Cleaning up temporary files and directories...")
for f_path in self.temp_files_to_clean:
if os.path.exists(f_path):
try: os.remove(f_path) # No sudo needed for /tmp files usually
except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}")
# Detach DMGs first
for dev_path in list(self.attached_dmg_devices): # Iterate copy
self._detach_dmg(dev_path)
self.attached_dmg_devices = []
for d_path in self.temp_dirs_to_clean:
if os.path.ismount(d_path):
try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30)
except Exception: pass # Ignore if already unmounted or error
if os.path.exists(d_path):
try: shutil.rmtree(d_path, ignore_errors=True)
except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}")
def _detach_dmg(self, device_path_or_mount_point):
if not device_path_or_mount_point: return
self._report_progress(f"Attempting to detach DMG associated with {device_path_or_mount_point}...")
try:
os.remove(f_path)
self._report_progress(f"Removed {f_path}")
except OSError as e:
self._report_progress(f"Error removing {f_path}: {e}")
# hdiutil detach can take a device path or sometimes a mount path if it's unique enough
# Using -force to ensure it detaches even if volumes are "busy" (after unmount attempts)
self._run_command(["hdiutil", "detach", device_path_or_mount_point, "-force"], check=False, timeout=30)
if device_path_or_mount_point in self.attached_dmg_devices: # Check if it was in our list
self.attached_dmg_devices.remove(device_path_or_mount_point)
# Also try to remove if it's a /dev/diskX path that got added
if device_path_or_mount_point.startswith("/dev/") and device_path_or_mount_point in self.attached_dmg_devices:
self.attached_dmg_devices.remove(device_path_or_mount_point)
def _unmount_path(self, mount_path_or_device, is_device=False, force=False):
target = mount_path_or_device
cmd_base = ["diskutil"]
action = "unmountDisk" if is_device else "unmount"
if force:
cmd = cmd_base + [action, "force", target]
else:
cmd = cmd_base + [action, target]
is_target_valid_for_unmount = (os.path.ismount(mount_path_or_device) and not is_device) or \
(is_device and os.path.exists(target))
if is_target_valid_for_unmount:
self._report_progress(f"Attempting to unmount {target} (Action: {action}, Force: {force})...")
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 = []
self._report_progress(f"Could not detach {device_path_or_mount_point}: {e}")
def check_dependencies(self):
self._report_progress("Checking dependencies (qemu-img, diskutil, hdiutil, rsync)...")
dependencies = ["qemu-img", "diskutil", "hdiutil", "rsync"]
missing_deps = []
for dep in dependencies:
if not shutil.which(dep):
missing_deps.append(dep)
self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd)...")
dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `qemu-img` might need to be installed (e.g., via Homebrew: `brew install qemu`). `diskutil`, `hdiutil`, `rsync` are usually standard on macOS."
self._report_progress(msg)
raise RuntimeError(msg)
self._report_progress("All critical dependencies found.")
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); raise RuntimeError(msg)
self._report_progress("All critical dependencies for macOS USB installer creation found.")
return True
def _get_partition_device_id(self, parent_disk_id_str: str, partition_label_or_type: str) -> str | None:
"""Finds partition device ID by Volume Name or Content Hint."""
target_disk_id = parent_disk_id_str.replace("/dev/", "")
self._report_progress(f"Searching for partition '{partition_label_or_type}' on disk '{target_disk_id}'")
def _get_gibmacos_product_folder(self) -> str | None:
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()): # 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:
result = self._run_command(["diskutil", "list", "-plist", target_disk_id], capture_output=True)
if not result.stdout:
self._report_progress(f"No stdout from diskutil list for {target_disk_id}")
return None
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}")
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", [])
if not isinstance(all_disks_and_partitions, list):
if plist_data.get("DeviceIdentifier") == target_disk_id:
all_disks_and_partitions = [plist_data]
else:
all_disks_and_partitions = []
self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...")
self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True)
hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs"));
if not hfs_files: raise RuntimeError(f"No .hfs file found from {basesystem_dmg_to_process}")
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)
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:
if vol_name and vol_name.strip().lower() == partition_label_or_type.strip().lower():
self._report_progress(f"Found partition by VolumeName: {vol_name} -> /dev/{device_id}")
return f"/dev/{device_id}"
if content_hint and content_hint.strip().lower() == partition_label_or_type.strip().lower():
self._report_progress(f"Found partition by Content type: {content_hint} -> /dev/{device_id}")
return f"/dev/{device_id}"
def _create_minimal_efi_template(self, efi_dir_path): # Same as linux version
self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}")
oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True)
for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True)
with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("")
with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("")
basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}}
try:
with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML)
self._report_progress("Created basic placeholder config.plist.")
except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}")
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:
try:
self.check_dependencies()
self._cleanup_all_mounts_and_mappings()
for mp in self.temp_mount_points_to_clean:
os.makedirs(mp, exist_ok=True)
self._cleanup_temp_files_and_dirs()
for mp_dir in self.temp_dirs_to_clean: # Use full list from constructor
os.makedirs(mp_dir, exist_ok=True)
self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!")
self._report_progress(f"Unmounting disk {self.device} (force)...")
self._unmount_path(self.device, is_device=True, force=True)
time.sleep(2)
self._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2)
self._report_progress(f"Partitioning {self.device} with GPT scheme...")
self._run_command([
"diskutil", "partitionDisk", self.device, "GPT",
"MS-DOS FAT32", "EFI", "551MiB",
"JHFS+", "macOS_USB", "0b"
], timeout=180)
time.sleep(3)
installer_vol_name = f"Install macOS {self.target_macos_version}"
self._report_progress(f"Partitioning {self.device} as GPT: EFI (FAT32, 551MB), '{installer_vol_name}' (HFS+)...")
self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3)
esp_partition_dev = self._get_partition_device_id(self.device, "EFI")
macos_partition_dev = self._get_partition_device_id(self.device, "macOS_USB")
if not (esp_partition_dev and os.path.exists(esp_partition_dev)):
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}")
# Get actual partition identifiers
disk_info_plist = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout
if not disk_info_plist: raise RuntimeError("Failed to get disk info after partitioning.")
disk_info = plistlib.loads(disk_info_plist.encode('utf-8'))
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}")
# --- Write EFI content ---
self._report_progress(f"Converting OpenCore QCOW2 ({self.opencore_qcow2_path}) to RAW ({self.opencore_raw_path})...")
self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path])
# --- Prepare macOS Installer Content ---
product_folder = self._get_gibmacos_product_folder()
source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg)")
if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG for BaseSystem extraction not found in download path.")
self._report_progress(f"Attaching RAW OpenCore image ({self.opencore_raw_path})...")
attach_cmd_efi = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.opencore_raw_path]
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)
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.")
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._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_opencore_mount, source_efi_partition_dev], timeout=30)
self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB...")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev])
self._report_progress(f"Mounting target USB ESP ({esp_partition_dev}) to {self.temp_usb_esp_mount}...")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev], timeout=30)
core_services_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices")
self._run_command(["sudo", "mkdir", "-p", core_services_path_usb])
source_efi_content_path = os.path.join(self.temp_opencore_mount, "EFI")
if not os.path.isdir(source_efi_content_path): source_efi_content_path = self.temp_opencore_mount
original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder)
if original_bs_dmg:
self._report_progress(f"Copying {original_bs_dmg} to {core_services_path_usb}/BaseSystem.dmg")
self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")])
original_bs_chunklist = original_bs_dmg.replace(".dmg", ".chunklist")
if os.path.exists(original_bs_chunklist):
self._report_progress(f"Copying {original_bs_chunklist} to {core_services_path_usb}/BaseSystem.chunklist")
self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")])
target_efi_dir_on_usb = os.path.join(self.temp_usb_esp_mount, "EFI")
self._report_progress(f"Copying EFI files from {source_efi_content_path} to {target_efi_dir_on_usb}...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{source_efi_content_path}/", f"{target_efi_dir_on_usb}/"])
install_info_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder)
if install_info_src:
self._report_progress(f"Copying InstallInfo.plist to {self.temp_usb_macos_target_mount}/InstallInfo.plist")
self._run_command(["sudo", "cp", install_info_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")])
self._unmount_path(self.temp_opencore_mount, force=True)
self._unmount_path(self.temp_usb_esp_mount, force=True)
self._detach_raw_image_device(raw_efi_disk_id); raw_efi_disk_id = None
packages_dir_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages")
self._run_command(["sudo", "mkdir", "-p", packages_dir_usb])
# --- Write macOS main image (File-level copy) ---
self._report_progress(f"Converting macOS QCOW2 ({self.macos_qcow2_path}) to RAW ({self.macos_raw_path})...")
self._report_progress("This may take a very long time...")
self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path])
# Copy main installer package(s) or app contents. This is simplified.
# A real createinstallmedia copies the .app then uses it. We are building manually.
# We need to find the main payload: InstallAssistant.pkg or InstallESD.dmg/SharedSupport.dmg content.
main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"], product_folder, "Main Installer Payload (PKG/DMG)")
if main_payload_src:
self._report_progress(f"Copying main payload {os.path.basename(main_payload_src)} to {packages_dir_usb}/")
self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb, os.path.basename(main_payload_src))])
# If it's SharedSupport.dmg, its contents might be what's needed in Packages or elsewhere.
# If InstallAssistant.pkg, it might need to be placed at root or specific app structure.
else: self._report_progress("Warning: Main installer payload not found. Installer may be incomplete.")
self._report_progress(f"Attaching RAW macOS image ({self.macos_raw_path})...")
attach_cmd_macos = ["hdiutil", "attach", "-nomount", "-imagekey", "diskimage-class=CRawDiskImage", self.macos_raw_path]
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)
self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")])
self._report_progress("macOS installer assets copied.")
source_macos_part_dev = self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS_Container") or \
self._get_partition_device_id(raw_macos_disk_id, "Apple_APFS") or \
self._get_partition_device_id(raw_macos_disk_id, "Apple_HFS") or \
f"{raw_macos_disk_id}s2"
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}")
# --- 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}"); self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir])
self._report_progress(f"Mounting source macOS partition ({source_macos_part_dev}) to {self.temp_macos_source_mount}...")
self._run_command(["diskutil", "mount", "readOnly", "-mountPoint", self.temp_macos_source_mount, source_macos_part_dev], timeout=60)
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
if not os.path.exists(temp_config_plist_path) and os.path.exists(os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist")):
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}...")
self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev], timeout=30)
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
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._report_progress("This will also take a very long time.")
self._run_command([
"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._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev])
self._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...")
self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"])
self._report_progress("USB writing process completed successfully.")
self._report_progress("USB Installer creation process completed successfully.")
return True
except Exception as e:
self._report_progress(f"An error occurred during USB writing on macOS: {e}")
import traceback
self._report_progress(traceback.format_exc())
self._report_progress(f"An error occurred during USB writing on macOS: {e}\n{traceback.format_exc()}")
return False
finally:
self._cleanup_all_mounts_and_mappings()
self._cleanup_temp_files()
self._cleanup_temp_files_and_dirs()
if __name__ == '__main__':
if platform.system() != "Darwin": print("This script is intended for macOS."); exit(1)
print("USB Writer macOS Standalone Test - File Copy Method")
import traceback
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"
mock_macos_path = "mock_macos_macos.qcow2"
if not os.path.exists(mock_opencore_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_opencore_path, "384M"])
if not os.path.exists(mock_macos_path): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_macos_path, "1G"])
# Create dummy BaseSystem.dmg inside the product folder's SharedSupport
dummy_bs_dmg_path = os.path.join(mock_product_folder_path, "SharedSupport", "BaseSystem.dmg")
if not os.path.exists(dummy_bs_dmg_path):
with open(dummy_bs_dmg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy DMG
print("\nAvailable disks (use 'diskutil list external physical' in Terminal to identify your USB):")
subprocess.run(["diskutil", "list", "external", "physical"], check=False)
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX - NOT /dev/diskXsY). THIS DISK WILL BE WIPED: ")
dummy_installinfo_path = os.path.join(mock_product_folder_path, "InstallInfo.plist")
if not os.path.exists(dummy_installinfo_path):
with open(dummy_installinfo_path, "wb") as f: plistlib.dump({"DisplayName":f"macOS {sys.argv[1] if len(sys.argv) > 1 else 'Sonoma'}"},f)
if not test_device or not test_device.startswith("/dev/disk"):
print("Invalid disk identifier. Exiting.")
if os.path.exists(mock_opencore_path): os.remove(mock_opencore_path)
if os.path.exists(mock_macos_path): os.remove(mock_macos_path)
exit(1)
if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR)
if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"))
dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist")
if not os.path.exists(dummy_config_template_path):
with open(dummy_config_template_path, "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): ")
success = False
if confirm.lower() == 'yes':
print("Ensure you have sudo privileges for rsync if needed, or app is run as root.")
writer = USBWriterMacOS(test_device, mock_opencore_path, mock_macos_path, print)
success = writer.format_and_write()
else:
print("Test cancelled.")
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.")
print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False)
test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ")
if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) # No need to clean OC_TEMPLATE_DIR here
if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes':
writer = USBWriterMacOS(test_device, mock_download_dir, print, True, sys.argv[1] if len(sys.argv) > 1 else "Sonoma")
writer.format_and_write()
else: print("Test cancelled.")
shutil.rmtree(mock_download_dir, ignore_errors=True)
print("Mock download dir cleaned up.")

View File

@ -1,48 +1,58 @@
# usb_writer_windows.py
# usb_writer_windows.py (Refactoring for Installer Workflow)
import subprocess
import os
import time
import shutil
import re # For parsing diskpart output
import re
import glob # For _find_gibmacos_asset
import traceback
import sys # For checking psutil import
# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test
try:
from PyQt6.QtWidgets import QMessageBox
from PyQt6.QtWidgets import QMessageBox # For user guidance
except ImportError:
class QMessageBox: # Mock for standalone testing
@staticmethod
def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'")
@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
No = 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:
def __init__(self, device_id: str, opencore_qcow2_path: str, macos_qcow2_path: str,
progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""):
# device_id is expected to be the disk number string, e.g., "1", "2" or "disk 1", "disk 2"
self.disk_number = "".join(filter(str.isdigit, device_id))
def __init__(self, device_id_str: str, macos_download_path: str,
progress_callback=None, enhance_plist_enabled: bool = False,
target_macos_version: str = ""):
# device_id_str is expected to be the disk number string from user, e.g., "1", "2"
self.disk_number = "".join(filter(str.isdigit, device_id_str))
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.opencore_qcow2_path = opencore_qcow2_path
self.macos_qcow2_path = macos_qcow2_path
self.macos_download_path = macos_download_path
self.progress_callback = progress_callback
self.enhance_plist_enabled = enhance_plist_enabled # Not used in Windows writer yet
self.target_macos_version = target_macos_version # Not used in Windows writer yet
self.enhance_plist_enabled = enhance_plist_enabled
self.target_macos_version = target_macos_version
pid = os.getpid()
self.opencore_raw_path = f"opencore_temp_{pid}.raw"
self.macos_raw_path = f"macos_main_temp_{pid}.raw"
self.temp_efi_extract_dir = f"temp_efi_files_{pid}"
self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs"
self.temp_efi_build_dir = f"temp_efi_build_{pid}"
self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For 7z extractions
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
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:
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)
output_text = "" # Initialize to empty string
try:
self._report_progress(f"Running diskpart script:\n{script_content}")
process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False)
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.
# This is not a perfect error check for diskpart.
success_indicators = [
"DiskPart successfully", "successfully completed", "succeeded in creating",
"successfully formatted", "successfully assigned"
]
success_indicators = ["DiskPart successfully", "successfully completed", "succeeded in creating", "successfully formatted", "successfully assigned"]
has_success_indicator = any(indicator in output_text for indicator in success_indicators)
has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text
if has_error_indicator:
self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}")
# Optionally raise an error here if script is critical
# 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
elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text :
self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}")
if capture_output_for_parse:
return output_text
if capture_output_for_parse: return output_text
finally:
if os.path.exists(script_file_path): os.remove(script_file_path)
return output_text if capture_output_for_parse else None # Return None if not capturing for parse
return output_text if capture_output_for_parse else None
def _cleanup_temp_files_and_dirs(self):
@ -113,8 +113,7 @@ class USBWriterWindows:
def _find_available_drive_letter(self) -> str | None:
import string; used_letters = set()
try:
# Check if psutil was imported by the main application
if 'psutil' in sys.modules:
if 'psutil' in sys.modules: # Check if psutil was imported by main app
partitions = sys.modules['psutil'].disk_partitions(all=True)
for p in partitions:
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":
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 None
def check_dependencies(self):
self._report_progress("Checking dependencies (qemu-img, diskpart, robocopy)... DD for Win & 7z are manual checks.")
dependencies = ["qemu-img", "diskpart", "robocopy"]; missing = [dep for dep in dependencies if not shutil.which(dep)]
if missing: raise RuntimeError(f"Missing dependencies: {', '.join(missing)}. qemu-img needs install & PATH.")
self._report_progress("Base dependencies found. Ensure 'dd for Windows' and '7z.exe' are in PATH if needed.")
self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...")
dependencies = ["diskpart", "robocopy", "7z"]
missing_deps = [dep for dep in dependencies if not shutil.which(dep)]
if missing_deps:
msg = f"Missing dependencies: {', '.join(missing_deps)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip CLI) needs to be installed and in PATH."
self._report_progress(msg); raise RuntimeError(msg)
self._report_progress("Base dependencies found. Ensure a 'dd for Windows' utility is installed and in your PATH for writing the main macOS BaseSystem image.")
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:
try:
self.check_dependencies()
self._cleanup_temp_files_and_dirs() # Clean before start
os.makedirs(self.temp_efi_extract_dir, exist_ok=True)
self._cleanup_temp_files_and_dirs()
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.assigned_efi_letter = self._find_available_drive_letter()
if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.")
self._report_progress(f"Will 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"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n"
diskpart_script_part1 += "create partition primary label=macOS_USB\nexit\n"
diskpart_script_part1 += f"create partition efi size=550 label=\"EFI\"\nformat fs=fat32 quick\nassign letter={self.assigned_efi_letter}\n" # Assign after format
diskpart_script_part1 += f"create partition primary label=\"Install macOS {self.target_macos_version}\" id=AF00\nexit\n" # Set HFS+ type ID
self._run_diskpart_script(diskpart_script_part1)
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)"
diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n"
detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True)
if detail_output:
self._report_progress(f"Detail Partition Output:\n{detail_output}")
offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE)
if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)"
# Try to find the line "Partition X" where X is the number we want
part_num_search = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE)
if part_num_search:
macos_partition_number_str = part_num_search.group(1)
part_num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE) # Match "Partition X" then "Type" on next line
if part_num_match:
macos_partition_number_str = part_num_match.group(1)
self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}")
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}")
self._run_command(["qemu-img", "convert", "-O", "raw", self.opencore_qcow2_path, self.opencore_raw_path])
temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist")
if not os.path.exists(temp_config_plist_path):
template_plist_src = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") # Name used in prior step
if os.path.exists(template_plist_src): shutil.copy2(template_plist_src, temp_config_plist_path)
else: self._create_minimal_efi_template(self.temp_efi_build_dir) # Fallback to create basic if template also missing
if shutil.which("7z"):
self._report_progress("Attempting EFI extraction using 7-Zip...")
self._run_command(["7z", "x", self.opencore_raw_path, f"-o{self.temp_efi_extract_dir}", "EFI", "-r", "-y"], check=False)
source_efi_folder = os.path.join(self.temp_efi_extract_dir, "EFI")
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.")
if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path):
self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only for this feature)...")
if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.")
else: self._report_progress("config.plist enhancement call failed or had issues.")
target_efi_on_usb = f"{self.assigned_efi_letter}:\\EFI"
if not os.path.exists(f"{self.assigned_efi_letter}:\\"): # Check if drive letter is mounted
time.sleep(3) # Wait a bit more
if not os.path.exists(f"{self.assigned_efi_letter}:\\"):
# Attempt to re-assign just in case
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)
target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\"
if not os.path.exists(target_efi_on_usb_root): # Wait and check again
time.sleep(3)
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_root):
raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible after assign.")
if not os.path.exists(target_efi_on_usb): os.makedirs(target_efi_on_usb, exist_ok=True)
self._report_progress(f"Copying EFI files from '{source_efi_folder}' to '{target_efi_on_usb}'")
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
else: raise RuntimeError("7-Zip CLI (7z.exe) not found in PATH for EFI extraction.")
self._report_progress(f"Copying final EFI folder to USB ESP ({target_efi_on_usb_root})...")
self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), target_efi_on_usb_root + "EFI", "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True)
self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}")
self._report_progress(f"Converting macOS QCOW2 to RAW: {self.macos_raw_path}")
self._run_command(["qemu-img", "convert", "-O", "raw", self.macos_qcow2_path, self.macos_raw_path])
# --- Prepare BaseSystem ---
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 = (
f"RAW macOS image conversion complete:\n'{abs_macos_raw_path}'\n\n"
f"Target USB: Disk {self.disk_number} (Path: {self.physical_drive_path})\n"
f"The target macOS partition is: Partition {macos_partition_number_str}\n"
f"Calculated Offset (approx): {macos_partition_offset_str}\n\n"
"MANUAL STEP REQUIRED using a 'dd for Windows' utility:\n"
"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
"2. Carefully identify your 'dd for Windows' utility and its exact syntax.\n"
" Common utilities: dd from SUSE (recommended), dd by chrysocome.net.\n"
"3. Example 'dd' command (SYNTAX VARIES GREATLY BETWEEN DD TOOLS!):\n"
f" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} bs=4M --progress`\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"
" 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" `dd if=\"{abs_macos_raw_path}\" of={self.physical_drive_path} seek=<PARTITION_OFFSET_IN_BLOCKS_OR_BYTES> bs=<YOUR_BLOCK_SIZE> ...`\n"
" (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."
f"EFI setup complete on drive {self.assigned_efi_letter}:.\n"
f"BaseSystem HFS image extracted to: '{abs_hfs_path}'.\n\n"
f"MANUAL STEP REQUIRED FOR MAIN macOS PARTITION:\n"
f"1. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n"
f"2. Use a 'dd for Windows' utility to write the extracted HFS image.\n"
f" Target: Disk {self.disk_number} (Path: {self.physical_drive_path}), Partition {macos_partition_number_str} (Offset: {macos_partition_offset_str}).\n"
f" Example command (VERIFY SYNTAX FOR YOUR DD TOOL!):\n"
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} --target-partition {macos_partition_number_str} bs=4M --progress` (Conceptual, if dd supports partition targeting by number)\n"
f" OR, if writing to the whole disk by offset (VERY ADVANCED & RISKY if offset is wrong):\n"
f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} seek=<OFFSET_IN_BLOCKS_OR_BYTES> bs=<YOUR_BLOCK_SIZE> ...` (Offset from diskpart is in bytes)\n\n"
"3. After writing BaseSystem, manually copy other installer files (like InstallAssistant.pkg or contents of SharedSupport.dmg) from "
f"'{self.macos_download_path}' to the 'Install macOS {self.target_macos_version}' partition on the USB. This requires a tool that can write to HFS+ partitions from Windows (e.g., TransMac, HFSExplorer, or do this from a Mac/Linux environment).\n\n"
"This tool CANNOT fully automate HFS+ partition writing or HFS+ file copying on Windows."
)
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
except Exception as e:
self._report_progress(f"Error during Windows USB writing: {e}")
import traceback; self._report_progress(traceback.format_exc())
self._report_progress(f"Error during Windows USB writing: {e}"); self._report_progress(traceback.format_exc())
return False
finally:
if self.assigned_efi_letter:
@ -244,26 +308,28 @@ class USBWriterWindows:
self._cleanup_temp_files_and_dirs()
if __name__ == '__main__':
if platform.system() != "Windows":
print("This script is for Windows standalone testing."); exit(1)
print("USB Writer Windows Standalone Test - Improved Guidance")
mock_oc = "mock_oc_win.qcow2"; mock_mac = "mock_mac_win.qcow2"
# Ensure qemu-img is available for mock file creation
if not shutil.which("qemu-img"):
print("qemu-img not found, cannot create mock files for test. Exiting.")
exit(1)
if not os.path.exists(mock_oc): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_oc, "384M"])
if not os.path.exists(mock_mac): subprocess.run(["qemu-img", "create", "-f", "qcow2", mock_mac, "1G"])
import traceback
from constants import MACOS_VERSIONS # Needed for _get_gibmacos_product_folder
if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1)
print("USB Writer Windows Standalone Test - Installer Method Guidance")
mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True)
target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma"
mock_product_name = f"000-00000 - macOS {target_version_cli} 14.x.x"
mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name)
os.makedirs(os.path.join(mock_product_folder, "SharedSupport"), exist_ok=True)
with open(os.path.join(mock_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg")
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 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_oc, mock_mac, print)
writer = USBWriterWindows(disk_id_input, mock_download_dir, print, True, target_version_cli)
writer.format_and_write()
else: print("Cancelled.")
if os.path.exists(mock_oc): os.remove(mock_oc)
if os.path.exists(mock_mac): os.remove(mock_mac)
print("Mocks cleaned.")
shutil.rmtree(mock_download_dir, ignore_errors=True)
# shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template
print("Mock download dir cleaned up.")