diff --git a/EFI_template_installer/EFI/OC/config-template.plist b/EFI_template_installer/EFI/OC/config-template.plist index a66fd57..bf1ea73 100644 --- a/EFI_template_installer/EFI/OC/config-template.plist +++ b/EFI_template_installer/EFI/OC/config-template.plist @@ -58,17 +58,11 @@ ArchAnyBundlePathLilu.kextCommentLiluEnabledExecutablePathContents/MacOS/LiluMaxKernelMinKernelPlistPathContents/Info.plist ArchAnyBundlePathVirtualSMC.kextCommentVirtualSMCEnabledExecutablePathContents/MacOS/VirtualSMCMaxKernelMinKernelPlistPathContents/Info.plist - ArchAnyBundlePathSMCProcessor.kextCommentSMCProcessor for CPU tempEnabledExecutablePathContents/MacOS/SMCProcessorMaxKernelMinKernelPlistPathContents/Info.plist - ArchAnyBundlePathSMCSuperIO.kextCommentSMCSuperIO for fan speedsEnabledExecutablePathContents/MacOS/SMCSuperIOMaxKernelMinKernelPlistPathContents/Info.plist ArchAnyBundlePathWhateverGreen.kextCommentWhateverGreen for GraphicsEnabledExecutablePathContents/MacOS/WhateverGreenMaxKernelMinKernelPlistPathContents/Info.plist - ArchAnyBundlePathAppleALC.kextCommentAppleALC for AudioEnabledExecutablePathContents/MacOS/AppleALCMaxKernelMinKernelPlistPathContents/Info.plist - ArchAnyBundlePathIntelMausi.kextCommentIntel EthernetEnabledExecutablePathContents/MacOS/IntelMausiMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathAppleALC.kextCommentAppleALC for AudioEnabledExecutablePathContents/MacOS/AppleALCMaxKernelMinKernelPlistPathContents/Info.plist + ArchAnyBundlePathIntelMausi.kextCommentIntel EthernetEnabledExecutablePathContents/MacOS/IntelMausiMaxKernelMinKernelPlistPathContents/Info.plist ArchAnyBundlePathRealtekRTL8111.kextCommentRealtek RTL8111EnabledExecutablePathContents/MacOS/RealtekRTL8111MaxKernelMinKernelPlistPathContents/Info.plist ArchAnyBundlePathLucyRTL8125Ethernet.kextCommentRealtek RTL8125 2.5GbEEnabledExecutablePathContents/MacOS/LucyRTL8125EthernetMaxKernelMinKernelPlistPathContents/Info.plist - ArchAnyBundlePathNVMeFix.kextCommentNVMe FixesEnabledExecutablePathContents/MacOS/NVMeFixMaxKernelMinKernelPlistPathContents/Info.plist - ArchAnyBundlePathCpuTopologyRebuild.kextCommentAlder Lake E-Core/P-Core fixEnabledExecutablePathContents/MacOS/CpuTopologyRebuildMaxKernelMinKernelPlistPathContents/Info.plist - ArchAnyBundlePathRestrictEvents.kextCommentRestrict unwanted eventsEnabledExecutablePathContents/MacOS/RestrictEventsMaxKernelMinKernelPlistPathContents/Info.plist - Block EmulateCpuid1DataCpuid1MaskDummyPowerManagementMaxKernelMinKernel @@ -101,9 +95,9 @@ SchemeCustomKernelFuzzyMatchKernelArchAutoKernelCacheAuto - MiscBlessOverrideBootConsoleAttributes0HibernateModeNoneHibernateSkipsPickerHideAuxiliaryLauncherOptionDisabledLauncherPathDefaultPickerAttributes17PickerAudioAssistPickerModeExternalPickerVariantAcidanthera\GoldenGatePollAppleHotKeysShowPickerTakeoffDelay0Timeout5DebugAppleDebugApplePanicDisableWatchDogDisplayDelay0DisplayLevel2147483650LogModules*SysReportTarget0EntriesSecurityAllowSetDefaultApECID0AuthRestartBlacklistAppleUpdateDmgLoadingSignedEnablePasswordExposeSensitiveData6HaltLevel2147483648PasswordHashPasswordSaltScanPolicy0SecureBootModelDisabledVaultOptionalSerialInitOverrideTools - NVRAMAdd4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14DefaultBackgroundColorAAAAAA==UIScaleAQ==7C436110-AB2A-4BBB-A880-FE41995C9F82SystemAudioVolumeRg==boot-args-v keepsyms=1 debug=0x100 alcid=1csr-active-configAAAAAA==prev-lang:kbdZW4tVVM6MA==run-efi-updaterNoDelete4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14UIScaleDefaultBackgroundColor7C436110-AB2A-4BBB-A880-FE41995C9F82boot-argscsr-active-configLegacyOverwriteLegacySchemaWriteFlash - PlatformInfoAutomaticCustomMemoryGenericAdviseFeaturesMLBCHANGE_ME_MLBMaxBIOSVersionProcessorType0ROMAAAAAA==SpoofVendorSystemMemoryStatusAutoSystemProductNameiMacPro1,1SystemSerialNumberCHANGE_ME_SERIALSystemUUIDCHANGE_ME_UUIDUpdateDataHubUpdateNVRAMUpdateSMBIOSUpdateSMBIOSModeCreateUseRawUuidEncoding - UEFIAPFSEnableJumpstartGlobalConnectHideVerboseJumpstartHotPlugMinDate0MinVersion0AppleInputAppleEventBuiltinCustomDelaysGraphicsInputMirroringKeyInitialDelay50KeySubsequentDelay5PointerSpeedDiv1PointerSpeedMul1AudioAudioCodec0AudioDeviceAudioOutMask-1AudioSupportDisconnectHdaMaximumGain-15MinimumAssistGain-30MinimumAudibleGain-55PlayChimeAutoResetTrafficClassSetupDelay0ConnectDriversDriversHfsPlus.efiOpenRuntime.efiOpenCanopy.efiInputKeyFilteringKeyForgetThreshold5KeySupportKeySupportModeAutoKeySwapPointerSupportPointerSupportModeASUSTimerResolution50000OutputClearScreenOnModeSwitchConsoleModeDirectGopRenderingForceResolutionGopPassThroughDisabledIgnoreTextInGraphicsProvideConsoleGopReconnectGraphicsOnConnectReconnectOnResChangeReplaceTabWithSpaceResolutionMaxSanitiseClearScreenTextRendererBuiltinGraphicsUIScale-1UgaPassThroughProtocolOverridesQuirksActivateHpetSupportDisableSecurityPolicyEnableVectorAccelerationEnableVmxExitBootServicesDelay0ForceOcWriteFlashForgeUefiSupportIgnoreInvalidFlexRatioReleaseUsbOwnership ReloadOptionRomsRequestBootVarRoutingResizeGpuBars-1TscSyncTimeout0UnblockFsConnectReservedMemory + PickerAudioAssistPickerModeExternalPickerVariantAutoPollAppleHotKeysShowPickerTakeoffDelay0Timeout5DebugAppleDebugApplePanicDisableWatchDogDisplayDelay0DisplayLevel2147483650LogModules*SysReportTarget0EntriesSecurityAllowSetDefaultApECID0AuthRestartBlacklistAppleUpdateDmgLoadingSignedEnablePasswordExposeSensitiveData6HaltLevel2147483648PasswordHashPasswordSaltScanPolicy0SecureBootModelDisabledVaultOptionalSerialInitOverrideTools + NVRAMAdd4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14DefaultBackgroundColorAAAAAA==UIScaleAQ==7C436110-AB2A-4BBB-A880-FE41995C9F82SystemAudioVolumeRg==boot-args-v keepsyms=1 debug=0x100 alcid=1csr-active-configAAAAAA==prev-lang:kbden-US:0run-efi-updaterNoDelete4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14UIScaleDefaultBackgroundColor7C436110-AB2A-4BBB-A880-FE41995C9F82boot-argscsr-active-configLegacyOverwriteLegacySchemaWriteFlash + SystemProductNameiMacPro1,1SystemSerialNumberCHANGEMESystemUUIDCHANGEMEUpdateDataHubUpdateNVRAMUpdateSMBIOSUpdateSMBIOSModeCreateUseRawUuidEncoding + UEFIAPFSEnableJumpstartGlobalConnectHideVerboseJumpstartHotPlugMinDate-1MinVersion-1AppleInputAppleEventBuiltinCustomDelaysGraphicsInputMirroringKeyInitialDelay50KeySubsequentDelay5PointerSpeedDiv1PointerSpeedMul1AudioAudioCodec0AudioDevicePciRoot(0x0)/Pci(0x1b,0x0)AudioOutMask1AudioSupportDisconnectHdaMaximumGain-15MinimumAssistGain-30MinimumAudibleGain-55PlayChimeAutoResetTrafficClassSetupDelay0ConnectDriversDriversHfsPlus.efiOpenRuntime.efiOpenCanopy.efiInputKeyFilteringKeyForgetThreshold5KeySupportKeySupportModeAutoKeySwapPointerSupportPointerSupportModeASUSTimerResolution50000OutputClearScreenOnModeSwitchConsoleModeDirectGopRenderingForceResolutionGopPassThroughDisabledIgnoreTextInGraphicsProvideConsoleGopReconnectGraphicsOnConnectReconnectOnResChangeReplaceTabWithSpaceResolutionMaxSanitiseClearScreenTextRendererBuiltinGraphicsUIScale0UgaPassThroughProtocolOverridesQuirksActivateHpetSupportDisableSecurityPolicyEnableVectorAccelerationEnableVmxExitBootServicesDelay0ForceOcWriteFlashForgeUefiSupportIgnoreInvalidFlexRatioReleaseUsbOwnershipReloadOptionRomsRequestBootVarRoutingResizeGpuBars-1TscSyncTimeout0UnblockFsConnectReservedMemory diff --git a/usb_writer_linux.py b/usb_writer_linux.py index 429cd6c..e0d1f08 100644 --- a/usb_writer_linux.py +++ b/usb_writer_linux.py @@ -1,4 +1,4 @@ -# usb_writer_linux.py (Refined asset copying) +# usb_writer_linux.py (Finalizing installer asset copying - refined) import subprocess import os import time @@ -12,61 +12,89 @@ try: from plist_modifier import enhance_config_plist except ImportError: enhance_config_plist = None - print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled for USBWriterLinux.") +# from constants import MACOS_VERSIONS # Imported in _get_gibmacos_product_folder OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") - class USBWriterLinux: def __init__(self, device: str, macos_download_path: str, progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): - self.device = device - self.macos_download_path = macos_download_path - self.progress_callback = progress_callback - self.enhance_plist_enabled = enhance_plist_enabled - self.target_macos_version = target_macos_version # String name like "Sonoma" - - pid = os.getpid() + self.device = device; self.macos_download_path = macos_download_path + self.progress_callback = progress_callback; self.enhance_plist_enabled = enhance_plist_enabled + self.target_macos_version = target_macos_version; pid = os.getpid() self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" self.temp_efi_build_dir = f"temp_efi_build_{pid}" - self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For extracting HFS from DMG - self.mount_point_usb_esp = f"/mnt/usb_esp_temp_skyscope_{pid}" self.mount_point_usb_macos_target = f"/mnt/usb_macos_target_temp_skyscope_{pid}" + self.temp_shared_support_mount = f"/mnt/shared_support_temp_{pid}" + self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # Added for _extract_hfs_from_dmg_or_pkg self.temp_files_to_clean = [self.temp_basesystem_hfs_path] self.temp_dirs_to_clean = [ self.temp_efi_build_dir, self.mount_point_usb_esp, - self.mount_point_usb_macos_target, self.temp_dmg_extract_dir + self.mount_point_usb_macos_target, self.temp_shared_support_mount, + self.temp_dmg_extract_dir # Ensure this is cleaned ] - def _report_progress(self, message: str): - if self.progress_callback: self.progress_callback(message) - else: print(message) + def _report_progress(self, message: str, is_rsync_line: bool = False): + if is_rsync_line: + match = re.search(r"(\d+)%\s+", message) + if match: + try: percentage = int(match.group(1)); self.progress_callback(f"PROGRESS_VALUE:{percentage}") + except ValueError: pass + if self.progress_callback: self.progress_callback(message) + else: print(message) + else: + if self.progress_callback: self.progress_callback(message) + else: print(message) - def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None): - self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}") - try: - process = subprocess.run( - command, check=check, capture_output=capture_output, text=True, timeout=timeout, - shell=shell, cwd=working_dir, - creationflags=0 - ) - if capture_output: - if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") - if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") - return process - except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise - except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise - except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise + def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None, stream_rsync_progress=False): + cmd_list = command if isinstance(command, list) else command.split() + is_rsync_progress_command = stream_rsync_progress and "rsync" in cmd_list[0 if cmd_list[0] != "sudo" else (1 if len(cmd_list) > 1 else 0)] + + if is_rsync_progress_command: + effective_cmd_list = list(cmd_list) + rsync_idx = -1 + for i, arg in enumerate(effective_cmd_list): + if "rsync" in arg: rsync_idx = i; break + if rsync_idx != -1: + conflicting_flags = ["-P", "--progress"]; effective_cmd_list = [arg for arg in effective_cmd_list if arg not in conflicting_flags] + actual_rsync_cmd_index_in_list = -1 + for i, arg_part in enumerate(effective_cmd_list): + if "rsync" in os.path.basename(arg_part): actual_rsync_cmd_index_in_list = i; break + if actual_rsync_cmd_index_in_list != -1: + if "--info=progress2" not in effective_cmd_list: effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--info=progress2") + if "--no-inc-recursive" not in effective_cmd_list : effective_cmd_list.insert(actual_rsync_cmd_index_in_list + 1, "--no-inc-recursive") + else: self._report_progress("Warning: rsync command part not found for progress flag insertion.") + self._report_progress(f"Executing (with progress streaming): {' '.join(effective_cmd_list)}") + process = subprocess.Popen(effective_cmd_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True, cwd=working_dir) + stdout_lines, stderr_lines = [], [] + if process.stdout: + for line in iter(process.stdout.readline, ''): line_strip = line.strip(); self._report_progress(line_strip, is_rsync_line=True); stdout_lines.append(line_strip) + process.stdout.close() + if process.stderr: + for line in iter(process.stderr.readline, ''): line_strip = line.strip(); self._report_progress(f"STDERR: {line_strip}"); stderr_lines.append(line_strip) + process.stderr.close() + return_code = process.wait(timeout=timeout); + if check and return_code != 0: raise subprocess.CalledProcessError(return_code, effective_cmd_list, output="\n".join(stdout_lines), stderr="\n".join(stderr_lines)) + return subprocess.CompletedProcess(args=effective_cmd_list, returncode=return_code, stdout="\n".join(stdout_lines), stderr="\n".join(stderr_lines)) + else: + self._report_progress(f"Executing: {' '.join(cmd_list)}") + try: + process = subprocess.run(cmd_list, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=0) + if capture_output: + if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") + if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") + return process + except subprocess.TimeoutExpired: self._report_progress(f"Command timed out after {timeout} seconds."); raise + except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise + except FileNotFoundError: self._report_progress(f"Error: Command '{cmd_list[0]}' not found."); raise def _cleanup_temp_files_and_dirs(self): - self._report_progress("Cleaning up temporary files and directories...") + self._report_progress("Cleaning up...") for mp in self.temp_dirs_to_clean: - if os.path.ismount(mp): - self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15) - + if os.path.ismount(mp): self._run_command(["sudo", "umount", "-lf", mp], check=False, timeout=15) for f_path in self.temp_files_to_clean: if os.path.exists(f_path): try: self._run_command(["sudo", "rm", "-f", f_path], check=False) @@ -76,318 +104,205 @@ class USBWriterLinux: try: self._run_command(["sudo", "rm", "-rf", d_path], check=False) except Exception as e: self._report_progress(f"Error removing temp dir {d_path}: {e}") - def check_dependencies(self): - self._report_progress("Checking dependencies (sgdisk, mkfs.vfat, mkfs.hfsplus, 7z, rsync, dd)...") - dependencies = ["sgdisk", "mkfs.vfat", "mkfs.hfsplus", "7z", "rsync", "dd"] - missing_deps = [dep for dep in dependencies if not shutil.which(dep)] - if missing_deps: - msg = f"Missing dependencies: {', '.join(missing_deps)}. Please install them (e.g., hfsprogs, p7zip-full, gdisk)." - self._report_progress(msg); raise RuntimeError(msg) - self._report_progress("All critical dependencies for Linux USB installer creation found.") - return True + def check_dependencies(self): self._report_progress("Checking deps...");deps=["sgdisk","parted","mkfs.vfat","mkfs.hfsplus","7z","rsync","dd"];m=[d for d in deps if not shutil.which(d)]; assert not m, f"Missing: {', '.join(m)}. Install hfsprogs for mkfs.hfsplus, p7zip for 7z."; return True - def _get_gibmacos_product_folder(self) -> str: - from constants import MACOS_VERSIONS # Import for this method - _report = self._report_progress - _report(f"Searching for macOS product folder in {self.macos_download_path} for version {self.target_macos_version}") - - # Check for a specific versioned download folder first (gibMacOS pattern) - # e.g. macOS Downloads/publicrelease/XXX - macOS Sonoma 14.X/ - possible_toplevel_folders = [ - os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease"), - os.path.join(self.macos_download_path, "macOS Downloads", "developerseed"), - os.path.join(self.macos_download_path, "macOS Downloads", "customerseed"), - self.macos_download_path # Fallback to searching directly in the provided path - ] - - version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower() - target_version_str_simple = self.target_macos_version.lower().replace("macos","").strip() - - - for base_path_to_search in possible_toplevel_folders: - if not os.path.isdir(base_path_to_search): continue - for item in os.listdir(base_path_to_search): - item_path = os.path.join(base_path_to_search, item) - item_lower = item.lower() - # Heuristic: look for version string or display name in folder name - if os.path.isdir(item_path) and \ - ("macos" in item_lower and (target_version_str_simple in item_lower or version_tag_from_constants in item_lower)): - _report(f"Identified gibMacOS product folder: {item_path}") - return item_path - - _report(f"Could not identify a specific product folder. Using base download path: {self.macos_download_path}") - return self.macos_download_path - - - def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str, search_deep=True) -> str | None: + def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None: if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] - self._report_progress(f"Searching for {asset_patterns} in {product_folder_path}...") - - # Prioritize direct children and common locations - common_subdirs = ["", "SharedSupport", "Install macOS*.app/Contents/SharedSupport", "Install macOS*.app/Contents/Resources"] - + search_base = product_folder_path or self.macos_download_path + self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") for pattern in asset_patterns: - for sub_dir_pattern in common_subdirs: - # Construct glob pattern, allowing for versioned app names - current_search_base = os.path.join(product_folder_path, sub_dir_pattern.replace("Install macOS*.app", f"Install macOS {self.target_macos_version}.app")) - # If the above doesn't exist, try generic app name for glob - if not os.path.isdir(os.path.dirname(current_search_base)) and "Install macOS*.app" in sub_dir_pattern: - current_search_base = os.path.join(product_folder_path, sub_dir_pattern) + common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"] + for sub_dir_pattern in common_subdirs_for_pattern: + current_search_base = os.path.join(search_base, sub_dir_pattern) + # Escape special characters for glob, but allow wildcards in pattern itself + # This simple escape might not be perfect for all glob patterns. + glob_pattern = os.path.join(glob.escape(current_search_base), pattern) - - glob_pattern = os.path.join(glob.escape(current_search_base), pattern) # Escape base path for glob - - # Search non-recursively first in specific paths found_files = glob.glob(glob_pattern, recursive=False) if found_files: - found_files.sort(key=os.path.getsize, reverse=True) # Prefer larger files if multiple (e.g. InstallESD.dmg) + found_files.sort(key=os.path.getsize, reverse=True) self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})") return found_files[0] - # If requested and not found yet, do a broader recursive search from product_folder_path if search_deep: - deep_search_pattern = os.path.join(glob.escape(product_folder_path), "**", pattern) - found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) # Prefer shallower paths + deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern) + found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) if found_files_deep: self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}") return found_files_deep[0] - self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {product_folder_path} or its common subdirectories.") + self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.") return None + def _get_gibmacos_product_folder(self) -> str | None: + from constants import MACOS_VERSIONS + base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease") + if not os.path.isdir(base_path): base_path = self.macos_download_path + if os.path.isdir(base_path): + for item in os.listdir(base_path): + item_path = os.path.join(base_path, item) + version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower() + if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag_from_constants in item.lower()): + self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path + self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path + def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: - # This method assumes dmg_or_pkg_path is the path to a file like BaseSystem.dmg, InstallESD.dmg, or InstallAssistant.pkg - # It tries to extract the core HFS+ filesystem (often '4.hfs' from BaseSystem.dmg) - os.makedirs(self.temp_dmg_extract_dir, exist_ok=True) - current_target_dmg = None - + os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path try: - if dmg_or_pkg_path.endswith(".pkg"): - self._report_progress(f"Extracting DMGs from PKG: {dmg_or_pkg_path}...") - self._run_command(["7z", "x", dmg_or_pkg_path, "*.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True) # Extract all DMGs recursively - dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*.dmg"), recursive=True) - if not dmgs_in_pkg: raise RuntimeError("No DMG found within PKG.") - - # Heuristic: find BaseSystem.dmg, else largest InstallESD.dmg, else largest SharedSupport.dmg - bs_dmg = next((d for d in dmgs_in_pkg if "basesystem.dmg" in d.lower()), None) - if bs_dmg: current_target_dmg = bs_dmg - else: - esd_dmgs = [d for d in dmgs_in_pkg if "installesd.dmg" in d.lower()] - if esd_dmgs: current_target_dmg = max(esd_dmgs, key=os.path.getsize) - else: - ss_dmgs = [d for d in dmgs_in_pkg if "sharedsupport.dmg" in d.lower()] - if ss_dmgs: current_target_dmg = max(ss_dmgs, key=os.path.getsize) # This might contain BaseSystem.dmg - else: current_target_dmg = max(dmgs_in_pkg, key=os.path.getsize) # Last resort: largest DMG - if not current_target_dmg: raise RuntimeError("Could not determine primary DMG within PKG.") - self._report_progress(f"Identified primary DMG from PKG: {current_target_dmg}") - elif dmg_or_pkg_path.endswith(".dmg"): - current_target_dmg = dmg_or_pkg_path - else: - raise RuntimeError(f"Unsupported file type for HFS extraction: {dmg_or_pkg_path}") - - # If current_target_dmg is (likely) InstallESD.dmg or SharedSupport.dmg, we need to find BaseSystem.dmg within it - basesystem_dmg_to_process = current_target_dmg - if "basesystem.dmg" not in os.path.basename(current_target_dmg).lower(): - self._report_progress(f"Searching for BaseSystem.dmg within {current_target_dmg}...") - # Extract to a sub-folder to avoid name clashes - nested_extract_dir = os.path.join(self.temp_dmg_extract_dir, "nested_dmg_contents") - os.makedirs(nested_extract_dir, exist_ok=True) - self._run_command(["7z", "e", current_target_dmg, "*BaseSystem.dmg", "-r", f"-o{nested_extract_dir}"], check=True) - found_bs_dmgs = glob.glob(os.path.join(nested_extract_dir, "**", "*BaseSystem.dmg"), recursive=True) - if not found_bs_dmgs: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target_dmg}") - basesystem_dmg_to_process = found_bs_dmgs[0] - self._report_progress(f"Located BaseSystem.dmg for processing: {basesystem_dmg_to_process}") - - self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}...") - self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True) - hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")) - if not hfs_files: # If no .hfs, maybe it's a flat DMG image already (unlikely for BaseSystem.dmg) - alt_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*")) - alt_files = [f for f in alt_files if os.path.isfile(f) and not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.getsize(f) > 2*1024*1024*1024] # Min 2GB - if alt_files: hfs_files = alt_files - if not hfs_files: raise RuntimeError(f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}") - - final_hfs_file = max(hfs_files, key=os.path.getsize) - self._report_progress(f"Found HFS+ partition image: {final_hfs_file}. Moving to {output_hfs_path}") - shutil.move(final_hfs_file, output_hfs_path) - return True - except Exception as e: - self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False + if dmg_or_pkg_path.endswith(".pkg"): self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}") + assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}" + basesystem_dmg_to_process = current_target + if "basesystem.dmg" not in os.path.basename(current_target).lower(): self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True); found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0] + self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); + if not hfs_files: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 2*1024*1024*1024] # Min 2GB HFS for BaseSystem + assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}" + final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True + except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False finally: if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) + def _create_minimal_efi_template(self, efi_dir_path): + self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}"); oc_dir=os.path.join(efi_dir_path,"EFI","OC");os.makedirs(os.path.join(efi_dir_path,"EFI","BOOT"),exist_ok=True);os.makedirs(oc_dir,exist_ok=True);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML) def format_and_write(self) -> bool: try: - self.check_dependencies() - self._cleanup_temp_files_and_dirs() - for mp_dir in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]: - self._run_command(["sudo", "mkdir", "-p", mp_dir]) - - self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") + self.check_dependencies(); self._cleanup_temp_files_and_dirs(); + for mp_dir in [self.mount_point_usb_esp, self.mount_point_usb_macos_target, self.temp_efi_build_dir]: self._run_command(["sudo", "mkdir", "-p", mp_dir]) + self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!"); for i in range(1, 10): self._run_command(["sudo", "umount", "-lf", f"{self.device}{i}"], check=False, timeout=5); self._run_command(["sudo", "umount", "-lf", f"{self.device}p{i}"], check=False, timeout=5) self._report_progress(f"Partitioning {self.device} with GPT (sgdisk)...") self._run_command(["sudo", "sgdisk", "--zap-all", self.device]) - self._run_command(["sudo", "sgdisk", "-n", "1:0:+550M", "-t", "1:ef00", "-c", "1:EFI", self.device]) + self._run_command(["sudo", "sgdisk", "-n", "0:0:+551MiB", "-t", "0:ef00", "-c", "0:EFI", self.device]) usb_vol_name = f"Install macOS {self.target_macos_version}" - self._run_command(["sudo", "sgdisk", "-n", "2:0:0", "-t", "2:af00", "-c", f"2:{usb_vol_name[:11]}" , self.device]) + self._run_command(["sudo", "sgdisk", "-n", "0:0:0", "-t", "0:af00", "-c", f"0:{usb_vol_name[:11]}" , self.device]) self._run_command(["sudo", "partprobe", self.device], timeout=10); time.sleep(3) + esp_dev=f"{self.device}1" if os.path.exists(f"{self.device}1") else f"{self.device}p1"; macos_part=f"{self.device}2" if os.path.exists(f"{self.device}2") else f"{self.device}p2"; assert os.path.exists(esp_dev) and os.path.exists(macos_part), "Partitions not found." + self._report_progress(f"Formatting ESP {esp_dev}..."); self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_dev]) + self._report_progress(f"Formatting macOS partition {macos_part}..."); self._run_command(["sudo", "mkfs.hfsplus", "-v", usb_vol_name, macos_part]) - esp_partition_dev = next((f"{self.device}{i}" for i in ["1", "p1"] if os.path.exists(f"{self.device}{i}")), None) - macos_partition_dev = next((f"{self.device}{i}" for i in ["2", "p2"] if os.path.exists(f"{self.device}{i}")), None) - if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not reliably determine partition names for {self.device}.") - - self._report_progress(f"Formatting ESP ({esp_partition_dev}) as FAT32...") - self._run_command(["sudo", "mkfs.vfat", "-F", "32", "-n", "EFI", esp_partition_dev]) - self._report_progress(f"Formatting macOS Install partition ({macos_partition_dev}) as HFS+...") - self._run_command(["sudo", "mkfs.hfsplus", "-v", usb_vol_name, macos_partition_dev]) - - product_folder = self._get_gibmacos_product_folder() - - source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)") - if not source_for_hfs_extraction: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.") - - if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path): + product_folder_path = self._get_gibmacos_product_folder() + basesystem_source_dmg_or_pkg = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)") + if not basesystem_source_dmg_or_pkg: raise RuntimeError("Essential macOS DMG/PKG for BaseSystem extraction not found in download path.") + if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path): raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.") + self._report_progress(f"Writing BaseSystem to {macos_part}..."); self._run_command(["sudo","dd",f"if={self.temp_basesystem_hfs_path}",f"of={macos_part}","bs=4M","status=progress","oflag=sync"]) + self._report_progress("Mounting macOS USB partition..."); self._run_command(["sudo","mount",macos_part,self.mount_point_usb_macos_target]) - self._report_progress(f"Writing BaseSystem HFS+ image to {macos_partition_dev} using dd...") - self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={macos_partition_dev}", "bs=4M", "status=progress", "oflag=sync"]) + # --- Finalizing macOS Installer Content on USB's HFS+ partition --- + self._report_progress("Finalizing macOS installer content on USB...") + usb_target_root = self.mount_point_usb_macos_target - self._report_progress("Mounting macOS Install partition on USB...") - self._run_command(["sudo", "mount", macos_partition_dev, self.mount_point_usb_macos_target]) - - # --- Copying full installer assets --- - self._report_progress("Copying macOS installer assets to USB...") - - # 1. Create "Install macOS [VersionName].app" structure app_bundle_name = f"Install macOS {self.target_macos_version}.app" - app_bundle_path_usb = os.path.join(self.mount_point_usb_macos_target, app_bundle_name) + app_bundle_path_usb = os.path.join(usb_target_root, app_bundle_name) contents_path_usb = os.path.join(app_bundle_path_usb, "Contents") shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport") - resources_path_usb_app = os.path.join(contents_path_usb, "Resources") - self._run_command(["sudo", "mkdir", "-p", shared_support_path_usb_app]) - self._run_command(["sudo", "mkdir", "-p", resources_path_usb_app]) + resources_path_usb_app = os.path.join(contents_path_usb, "Resources") # For createinstallmedia structure + sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages") + coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices") - # 2. Copy BaseSystem.dmg & BaseSystem.chunklist - core_services_path_usb = os.path.join(self.mount_point_usb_macos_target, "System", "Library", "CoreServices") - self._run_command(["sudo", "mkdir", "-p", core_services_path_usb]) - original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder) - if original_bs_dmg: - self._report_progress(f"Copying BaseSystem.dmg to {core_services_path_usb}/ and {shared_support_path_usb_app}/") - self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(core_services_path_usb, "BaseSystem.dmg")]) - self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")]) - original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg)) # Look in same dir as BaseSystem.dmg - if original_bs_chunklist: - self._report_progress(f"Copying BaseSystem.chunklist...") - self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(core_services_path_usb, "BaseSystem.chunklist")]) - self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")]) - else: self._report_progress("Warning: Original BaseSystem.dmg not found to copy.") + for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]: + self._run_command(["sudo", "mkdir", "-p", p]) - # 3. Copy InstallInfo.plist - installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder) + # Copy BaseSystem.dmg & BaseSystem.chunklist + bs_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True) + bs_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=True) + if bs_dmg_src: + self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...") + self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(coreservices_path_usb, "BaseSystem.dmg")]) + self._run_command(["sudo", "cp", bs_dmg_src, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")]) + if bs_chunklist_src: + self._report_progress(f"Copying BaseSystem.chunklist to USB CoreServices and App SharedSupport...") + self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(coreservices_path_usb, "BaseSystem.chunklist")]) + self._run_command(["sudo", "cp", bs_chunklist_src, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")]) + if not bs_dmg_src or not bs_chunklist_src: self._report_progress("Warning: BaseSystem.dmg or .chunklist not found in product folder.") + + # Copy InstallInfo.plist + installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True) if installinfo_src: self._report_progress(f"Copying InstallInfo.plist...") - self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]) # For .app bundle - self._run_command(["sudo", "cp", installinfo_src, os.path.join(self.mount_point_usb_macos_target, "InstallInfo.plist")]) # For root of volume - else: self._report_progress("Warning: InstallInfo.plist not found.") + self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]) + self._run_command(["sudo", "cp", installinfo_src, os.path.join(usb_target_root, "InstallInfo.plist")]) + else: self._report_progress("Warning: InstallInfo.plist (source) not found.") - # 4. Copy main installer package(s) to .app/Contents/SharedSupport/ - # And also to /System/Installation/Packages/ for direct BaseSystem boot. - packages_dir_usb_system = os.path.join(self.mount_point_usb_macos_target, "System", "Installation", "Packages") - self._run_command(["sudo", "mkdir", "-p", packages_dir_usb_system]) + # Copy main installer package(s) + main_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=True) or self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=True) + if main_pkg_src: + pkg_basename = os.path.basename(main_pkg_src) + self._report_progress(f"Copying main payload '{pkg_basename}' to App SharedSupport and System Packages...") + self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)]) + self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)]) + else: self._report_progress("Warning: Main installer package (InstallAssistant.pkg/InstallESD.dmg) not found.") - main_payload_patterns = ["InstallAssistant.pkg", "InstallESD.dmg", "SharedSupport.dmg"] # Order of preference - main_payload_src = self._find_gibmacos_asset(main_payload_patterns, product_folder, "Main Installer Payload (PKG/DMG)") + diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True) + if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")]) - if main_payload_src: - payload_basename = os.path.basename(main_payload_src) - self._report_progress(f"Copying main payload '{payload_basename}' to {shared_support_path_usb_app}/ and {packages_dir_usb_system}/") - self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)]) - self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb_system, payload_basename)]) - # If it's SharedSupport.dmg, its *contents* are often what's needed in Packages, not the DMG itself. - # This is a complex step; createinstallmedia does more. For now, copying the DMG/PKG might be enough for OpenCore to find. - else: self._report_progress("Warning: Main installer payload (InstallAssistant.pkg, InstallESD.dmg, or SharedSupport.dmg) not found.") + template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi") + if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0: + self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")]) + else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.") - # 5. Copy AppleDiagnostics.dmg to .app/Contents/SharedSupport/ - diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder) - if diag_src: - self._report_progress(f"Copying AppleDiagnostics.dmg to {shared_support_path_usb_app}/") - self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")]) - - # 6. Ensure /System/Library/CoreServices/boot.efi exists (can be a copy of OpenCore's BOOTx64.efi or a generic one) - self._report_progress("Ensuring /System/Library/CoreServices/boot.efi exists on installer partition...") - self._run_command(["sudo", "touch", os.path.join(core_services_path_usb, "boot.efi")]) # Placeholder, OC will handle actual boot - - self._report_progress("macOS installer assets copied to USB.") + # Create .IAProductInfo (Simplified XML string to avoid f-string issues in tool call) + ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo") + ia_content_xml = "Product IDcom.apple.pkg.InstallAssistantProduct Path" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg" + temp_ia_path = f"temp_iaproductinfo_{pid}.plist" + with open(temp_ia_path, "w") as f: f.write(ia_content_xml) + self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path]) + if os.path.exists(temp_ia_path): os.remove(temp_ia_path) + self._report_progress("Created .IAProductInfo.") + self._report_progress("macOS installer assets fully copied to USB.") # --- OpenCore EFI Setup --- - self._report_progress("Setting up OpenCore EFI on ESP...") + self._report_progress("Setting up OpenCore EFI on ESP..."); self._run_command(["sudo", "mount", esp_dev, self.mount_point_usb_esp]) if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) - else: self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}"); self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) + else: self._run_command(["sudo", "cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") if not os.path.exists(temp_config_plist_path): template_plist = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") - if os.path.exists(template_plist): self._run_command(["sudo", "cp", template_plist, temp_config_plist_path]) - else: - with open(temp_config_plist_path, 'wb') as f: plistlib.dump({"#Comment": "Basic config by Skyscope"}, f, fmt=plistlib.PlistFormat.XML); os.chmod(temp_config_plist_path, 0o644) # Ensure permissions - if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): + if os.path.exists(template_plist): shutil.copy2(template_plist, temp_config_plist_path) + else: plistlib.dump({"#Comment": "Basic config by Skyscope"}, open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML) + if self.enhance_plist_enabled and enhance_config_plist: self._report_progress("Attempting to enhance config.plist...") - if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement successful.") - else: self._report_progress("config.plist enhancement failed or had issues.") - self._run_command(["sudo", "mount", esp_partition_dev, self.mount_point_usb_esp]) + if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.") + else: self._report_progress("config.plist enhancement call failed or had issues.") self._report_progress(f"Copying final EFI folder to USB ESP ({self.mount_point_usb_esp})...") - self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"]) + self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mount_point_usb_esp}/EFI/"], stream_rsync_progress=True) self._report_progress("USB Installer creation process completed successfully.") return True except Exception as e: - self._report_progress(f"An error occurred during USB writing: {e}\n{traceback.format_exc()}") + self._report_progress(f"An error occurred during USB writing: {e}"); self._report_progress(traceback.format_exc()) return False finally: self._cleanup_temp_files_and_dirs() if __name__ == '__main__': - # ... (Standalone test block needs constants.MACOS_VERSIONS for _get_gibmacos_product_folder) - from constants import MACOS_VERSIONS # For standalone test - import traceback + import traceback; from constants import MACOS_VERSIONS if os.geteuid() != 0: print("Please run this script as root (sudo) for testing."); exit(1) - print("USB Writer Linux Standalone Test - Installer Method (Fuller Asset Copying)") + print("USB Writer Linux Standalone Test - Installer Method (Fuller Asset Copying Logic)") mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) - target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" # Example: python usb_writer_linux.py Sonoma - - mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() # e.g. "sonoma" or "14" + target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" + mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() mock_product_name = f"012-34567 - macOS {target_version_cli} {mock_product_name_segment}.x.x" specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) - os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True) - os.makedirs(specific_product_folder, exist_ok=True) - + os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True); os.makedirs(specific_product_folder, exist_ok=True) with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(10*1024*1024)) with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.chunklist"), "w") as f: f.write("dummy chunklist") with open(os.path.join(specific_product_folder, "InstallInfo.plist"), "wb") as f: plistlib.dump({"DisplayName":f"macOS {target_version_cli}"},f) with open(os.path.join(specific_product_folder, "InstallAssistant.pkg"), "wb") as f: f.write(os.urandom(1024)) with open(os.path.join(specific_product_folder, "SharedSupport", "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1024)) - - - if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR) - if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")) - dummy_config_template_path = os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config.plist") - if not os.path.exists(dummy_config_template_path): - with open(dummy_config_template_path, "w") as f: f.write("TestTemplate") - + if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True) + if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True) + if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT"), exist_ok=True) + with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML) + with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("dummy bootx64.efi") print("\nAvailable block devices (be careful!):"); subprocess.run(["lsblk", "-d", "-o", "NAME,SIZE,MODEL"], check=True) test_device = input("\nEnter target device (e.g., /dev/sdX). THIS DEVICE WILL BE WIPED: ") - - if not test_device or not test_device.startswith("/dev/"): - print("Invalid device. Exiting.") - else: - confirm = input(f"Are you absolutely sure you want to wipe {test_device} and create installer for {target_version_cli}? (yes/NO): ") - success = False - if confirm.lower() == 'yes': - writer = USBWriterLinux(device=test_device, macos_download_path=mock_download_dir, progress_callback=print, enhance_plist_enabled=True, target_macos_version=target_version_cli) - success = writer.format_and_write() - else: print("Test cancelled by user.") - print(f"Test finished. Success: {success}") - - if os.path.exists(mock_download_dir): shutil.rmtree(mock_download_dir, ignore_errors=True) + if not test_device or not test_device.startswith("/dev/"): print("Invalid device."); shutil.rmtree(mock_download_dir); shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True); exit(1) + if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes': + writer = USBWriterLinux(test_device, mock_download_dir, print, True, target_version_cli) + writer.format_and_write() + else: print("Test cancelled.") + shutil.rmtree(mock_download_dir, ignore_errors=True); + # shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Usually keep template dir for other tests print("Mock download dir cleaned up.") diff --git a/usb_writer_macos.py b/usb_writer_macos.py index 104d2f8..aa5353a 100644 --- a/usb_writer_macos.py +++ b/usb_writer_macos.py @@ -13,17 +13,12 @@ except ImportError: enhance_config_plist = None print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.") -# Assumed to exist relative to this script or project root OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") -# For _get_gibmacos_product_folder to access MACOS_VERSIONS from constants.py -# This is a bit of a hack for a library module. Ideally, constants are passed or structured differently. try: from constants import MACOS_VERSIONS except ImportError: - # Define a fallback or minimal version if constants.py is not found in this context - # This might happen if usb_writer_macos.py is tested truly standalone without the full app structure. - MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"} # Example + MACOS_VERSIONS = {"Sonoma": "14", "Ventura": "13", "Monterey": "12"} print("Warning: constants.py not found, using fallback MACOS_VERSIONS for _get_gibmacos_product_folder.") @@ -31,32 +26,31 @@ class USBWriterMacOS: def __init__(self, device: str, macos_download_path: str, progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): - self.device = device # e.g., /dev/diskX + self.device = device self.macos_download_path = macos_download_path self.progress_callback = progress_callback self.enhance_plist_enabled = enhance_plist_enabled - self.target_macos_version = target_macos_version # Display name like "Sonoma" + self.target_macos_version = target_macos_version pid = os.getpid() - # Using /tmp for macOS temporary files self.temp_basesystem_hfs_path = f"/tmp/temp_basesystem_{pid}.hfs" self.temp_efi_build_dir = f"/tmp/temp_efi_build_{pid}" - self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" # For 7z extractions + self.temp_dmg_extract_dir = f"/tmp/temp_dmg_extract_{pid}" - # Mount points will be dynamically created by diskutil or hdiutil attach - # We just need to track them for cleanup if they are custom /tmp paths - self.mount_point_usb_esp = f"/tmp/usb_esp_temp_skyscope_{pid}" # Or use /Volumes/EFI - self.mount_point_usb_macos_target = f"/tmp/usb_macos_target_temp_skyscope_{pid}" # Or use /Volumes/Install macOS ... + self.mounted_usb_esp_path = None # Will be like /Volumes/EFI + self.mounted_usb_macos_path = None # Will be like /Volumes/Install macOS ... + self.mounted_source_basesystem_path = f"/tmp/source_basesystem_mount_{pid}" self.temp_files_to_clean = [self.temp_basesystem_hfs_path] self.temp_dirs_to_clean = [ self.temp_efi_build_dir, self.temp_dmg_extract_dir, - self.mount_point_usb_esp, self.mount_point_usb_macos_target - # Mount points created by diskutil mount are usually in /Volumes/ and unmounted by name + self.mounted_source_basesystem_path + # Actual USB mount points (/Volumes/EFI, /Volumes/Install macOS...) are unmounted, not rmdir'd from here ] self.attached_dmg_devices = [] # Store device paths from hdiutil attach - def _report_progress(self, message: str): + def _report_progress(self, message: str, is_rsync_line: bool = False): + # Simplified progress for macOS writer for now, can add rsync parsing later if needed if self.progress_callback: self.progress_callback(message) else: print(message) @@ -74,40 +68,79 @@ class USBWriterMacOS: def _cleanup_temp_files_and_dirs(self): self._report_progress("Cleaning up temporary files, directories, and mounts on macOS...") - for f_path in self.temp_files_to_clean: - if os.path.exists(f_path): - try: os.remove(f_path) - except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}") + + # Unmount our specific /tmp mount points first + if self.mounted_source_basesystem_path and os.path.ismount(self.mounted_source_basesystem_path): + self._unmount_path(self.mounted_source_basesystem_path, force=True) + # System mount points like /Volumes/EFI or /Volumes/Install macOS... are unmounted by diskutil unmountDisk or unmount + # We also add them to temp_dirs_to_clean if we used their dynamic path for rmdir later (but only if they were /tmp based) for dev_path in list(self.attached_dmg_devices): self._detach_dmg(dev_path) self.attached_dmg_devices = [] + for f_path in self.temp_files_to_clean: + if os.path.exists(f_path): + try: os.remove(f_path) + except OSError as e: self._report_progress(f"Error removing temp file {f_path}: {e}") + for d_path in self.temp_dirs_to_clean: - if os.path.ismount(d_path): - try: self._run_command(["diskutil", "unmount", "force", d_path], check=False, timeout=30) - except Exception: pass - if os.path.exists(d_path): + if os.path.exists(d_path) and d_path.startswith("/tmp/"): # Only remove /tmp dirs we created try: shutil.rmtree(d_path, ignore_errors=True) except OSError as e: self._report_progress(f"Error removing temp dir {d_path}: {e}") - def _detach_dmg(self, device_path_or_mount_point): - if not device_path_or_mount_point: return - self._report_progress(f"Attempting to detach DMG: {device_path_or_mount_point}...") + def _unmount_path(self, mount_path_or_device, is_device=False, force=False): + target = mount_path_or_device + cmd_base = ["diskutil"] + action = "unmountDisk" if is_device else "unmount" + cmd = cmd_base + ([action, "force", target] if force else [action, target]) + + # Check if it's a valid target for unmount/unmountDisk + # For mount paths, check os.path.ismount. For devices, check if base device exists. + can_unmount = False + if is_device: + # Extract base disk identifier like /dev/diskX from /dev/diskXsY + base_device = re.match(r"(/dev/disk\d+)", target) + if base_device and os.path.exists(base_device.group(1)): + can_unmount = True + elif os.path.ismount(target): + can_unmount = True + + if can_unmount: + self._report_progress(f"Attempting to {action} {'forcefully ' if force else ''}{target}...") + self._run_command(cmd, check=False, timeout=60) # Increased timeout for diskutil + else: + self._report_progress(f"Skipping unmount for {target}, not a valid mount point or device for this action.") + + + def _detach_dmg(self, device_path): + if not device_path or not device_path.startswith("/dev/disk"): return + self._report_progress(f"Attempting to detach DMG device {device_path}...") try: - if os.path.ismount(device_path_or_mount_point): - self._run_command(["diskutil", "unmount", "force", device_path_or_mount_point], check=False) - if device_path_or_mount_point.startswith("/dev/disk"): - self._run_command(["hdiutil", "detach", device_path_or_mount_point, "-force"], check=False, timeout=30) - if device_path_or_mount_point in self.attached_dmg_devices: - self.attached_dmg_devices.remove(device_path_or_mount_point) + # Ensure it's actually a virtual disk from hdiutil + is_virtual_disk = False + try: + info_result = self._run_command(["diskutil", "info", "-plist", device_path], capture_output=True) + if info_result.returncode == 0 and info_result.stdout: + disk_info = plistlib.loads(info_result.stdout.encode('utf-8')) + if disk_info.get("VirtualOrPhysical") == "Virtual": + is_virtual_disk = True + except Exception: pass # Ignore parsing errors, proceed to detach attempt + + if is_virtual_disk: + self._run_command(["hdiutil", "detach", device_path, "-force"], check=False, timeout=30) + else: + self._report_progress(f"{device_path} is not a virtual disk, or info check failed. Skipping direct hdiutil detach.") + + if device_path in self.attached_dmg_devices: + self.attached_dmg_devices.remove(device_path) except Exception as e: - self._report_progress(f"Could not detach/unmount {device_path_or_mount_point}: {e}") + self._report_progress(f"Could not detach {device_path}: {e}") def check_dependencies(self): - self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd)...") - dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd"] + self._report_progress("Checking dependencies (diskutil, hdiutil, 7z, rsync, dd, bless)...") + dependencies = ["diskutil", "hdiutil", "7z", "rsync", "dd", "bless"] missing_deps = [dep for dep in dependencies if not shutil.which(dep)] if missing_deps: msg = f"Missing dependencies: {', '.join(missing_deps)}. `7z` (p7zip) might need to be installed (e.g., via Homebrew: `brew install p7zip`). Others are standard." @@ -124,34 +157,28 @@ class USBWriterMacOS: version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower() if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()): self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path - self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}' in {base_path}. Using general download path: {self.macos_download_path}"); return self.macos_download_path + self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None: if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] search_base = product_folder_path or self.macos_download_path self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") for pattern in asset_patterns: - common_subdirs_for_pattern = ["", "SharedSupport"] # Most assets are here or root of product folder - if "Install macOS" in pattern : # If looking for the .app bundle itself - common_subdirs_for_pattern = [""] # Only look at root of product folder - + common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"] for sub_dir_pattern in common_subdirs_for_pattern: current_search_base = os.path.join(search_base, sub_dir_pattern) glob_pattern = os.path.join(glob.escape(current_search_base), pattern) - found_files = glob.glob(glob_pattern, recursive=False) if found_files: found_files.sort(key=os.path.getsize, reverse=True) self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})") return found_files[0] - if search_deep: deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern) found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) if found_files_deep: self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}") return found_files_deep[0] - self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.") return None @@ -159,55 +186,34 @@ class USBWriterMacOS: os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path try: if dmg_or_pkg_path.endswith(".pkg"): - self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True) - dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); - if not dmgs_in_pkg: raise RuntimeError("No DMG found in PKG.") - current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) or dmgs_in_pkg[0] - if not current_target: raise RuntimeError("Could not determine primary DMG in PKG.") - self._report_progress(f"Using DMG from PKG: {current_target}") - if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}") - + self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True); dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); assert dmgs_in_pkg, "No DMG in PKG."; current_target = max(dmgs_in_pkg, key=os.path.getsize, default=dmgs_in_pkg[0]); assert current_target, "No primary DMG in PKG."; self._report_progress(f"Using DMG from PKG: {current_target}") + assert current_target and current_target.endswith(".dmg"), f"Not a valid DMG: {current_target}" basesystem_dmg_to_process = current_target if "basesystem.dmg" not in os.path.basename(current_target).lower(): - self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True) # Recursive search - found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True) - if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}") - basesystem_dmg_to_process = found_bs_dmg[0] - - self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True) - hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); - if not hfs_files: raise RuntimeError(f"No .hfs file found from {basesystem_dmg_to_process}") + self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True); found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True); assert found_bs_dmg, f"No BaseSystem.dmg from {current_target}"; basesystem_dmg_to_process = found_bs_dmg[0] + self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); + if not hfs_files: self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True); hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 2*1024*1024*1024] + assert hfs_files, f"No suitable HFS+ image file found after extracting {basesystem_dmg_to_process}" final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False finally: if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) - def _create_minimal_efi_template(self, efi_dir_path): - self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}") - oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True) - for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True) - with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("") - with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("") - basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}} - try: - with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML) - self._report_progress("Created basic placeholder config.plist.") - except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}") - + self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}"); oc_dir=os.path.join(efi_dir_path,"EFI","OC");os.makedirs(os.path.join(efi_dir_path,"EFI","BOOT"),exist_ok=True);os.makedirs(oc_dir,exist_ok=True);[os.makedirs(os.path.join(oc_dir,s),exist_ok=True) for s in ["Drivers","Kexts","ACPI","Tools","Resources"]];open(os.path.join(efi_dir_path,"EFI","BOOT","BOOTx64.efi"),"w").close();open(os.path.join(oc_dir,"OpenCore.efi"),"w").close();bc={"#Comment":"Basic config","Misc":{"Security":{"ScanPolicy":0,"SecureBootModel":"Disabled"}},"PlatformInfo":{"Generic":{"MLB":"CHANGE_ME_MLB","SystemSerialNumber":"CHANGE_ME_SERIAL","SystemUUID":"CHANGE_ME_UUID","ROM":b"\0"*6}}};plistlib.dump(bc,open(os.path.join(oc_dir,"config.plist"),'wb'),fmt=plistlib.PlistFormat.XML) def format_and_write(self) -> bool: try: self.check_dependencies() self._cleanup_temp_files_and_dirs() for mp_dir in self.temp_dirs_to_clean: - os.makedirs(mp_dir, exist_ok=True) + if not os.path.exists(mp_dir): os.makedirs(mp_dir, exist_ok=True) self._report_progress(f"WARNING: ALL DATA ON {self.device} WILL BE ERASED!") self._run_command(["diskutil", "unmountDisk", "force", self.device], check=False, timeout=60); time.sleep(2) installer_vol_name = f"Install macOS {self.target_macos_version}" - self._report_progress(f"Partitioning {self.device} as GPT: EFI (FAT32, 551MB), '{installer_vol_name}' (HFS+)...") + self._report_progress(f"Partitioning {self.device} for '{installer_vol_name}'...") self._run_command(["diskutil", "partitionDisk", self.device, "GPT", "FAT32", "EFI", "551MiB", "JHFS+", installer_vol_name, "0b"], timeout=180); time.sleep(3) disk_info_plist_str = self._run_command(["diskutil", "list", "-plist", self.device], capture_output=True).stdout @@ -215,11 +221,10 @@ class USBWriterMacOS: disk_info = plistlib.loads(disk_info_plist_str.encode('utf-8')) esp_partition_dev = None; macos_partition_dev = None - # Find the main disk entry first main_disk_entry = next((d for d in disk_info.get("AllDisksAndPartitions", []) if d.get("DeviceIdentifier") == self.device.replace("/dev/", "")), None) if main_disk_entry: for part in main_disk_entry.get("Partitions", []): - if part.get("VolumeName") == "EFI" and part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" + if part.get("Content") == "EFI": esp_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" elif part.get("VolumeName") == installer_vol_name: macos_partition_dev = f"/dev/{part.get('DeviceIdentifier')}" if not (esp_partition_dev and macos_partition_dev): raise RuntimeError(f"Could not identify partitions on {self.device} (EFI: {esp_partition_dev}, macOS: {macos_partition_dev}). Check diskutil list output.") @@ -236,51 +241,60 @@ class USBWriterMacOS: self._report_progress(f"Writing BaseSystem HFS+ image to {raw_macos_partition_dev} using dd...") self._run_command(["sudo", "dd", f"if={self.temp_basesystem_hfs_path}", f"of={raw_macos_partition_dev}", "bs=1m"], timeout=1800) - self._report_progress(f"Mounting macOS Install partition ({macos_partition_dev}) on USB to {self.temp_usb_macos_target_mount}...") - self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev]) + self.mounted_usb_macos_path = f"/Volumes/{installer_vol_name}" + if not os.path.ismount(self.mounted_usb_macos_path): + self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_macos_target_mount, macos_partition_dev]) + self.mounted_usb_macos_path = self.temp_usb_macos_target_mount - self._report_progress("Copying necessary macOS installer assets to USB...") + self._report_progress(f"macOS partition mounted at {self.mounted_usb_macos_path}") + + usb_target_root = self.mounted_usb_macos_path app_bundle_name = f"Install macOS {self.target_macos_version}.app" - app_bundle_path_usb = os.path.join(self.temp_usb_macos_target_mount, app_bundle_name) + app_bundle_path_usb = os.path.join(usb_target_root, app_bundle_name) contents_path_usb = os.path.join(app_bundle_path_usb, "Contents") shared_support_path_usb_app = os.path.join(contents_path_usb, "SharedSupport") - self._run_command(["sudo", "mkdir", "-p", shared_support_path_usb_app]) - self._run_command(["sudo", "mkdir", "-p", os.path.join(contents_path_usb, "Resources")]) + resources_path_usb_app = os.path.join(contents_path_usb, "Resources") + sys_install_pkgs_usb = os.path.join(usb_target_root, "System", "Installation", "Packages") + coreservices_path_usb = os.path.join(usb_target_root, "System", "Library", "CoreServices") - coreservices_path_usb = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices") - self._run_command(["sudo", "mkdir", "-p", coreservices_path_usb]) + for p in [shared_support_path_usb_app, resources_path_usb_app, coreservices_path_usb, sys_install_pkgs_usb]: + self._run_command(["sudo", "mkdir", "-p", p]) - original_bs_dmg = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=True) - if original_bs_dmg: - self._report_progress(f"Copying BaseSystem.dmg to USB CoreServices and App SharedSupport...") - self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(coreservices_path_usb, "BaseSystem.dmg")]) - self._run_command(["sudo", "cp", original_bs_dmg, os.path.join(shared_support_path_usb_app, "BaseSystem.dmg")]) - original_bs_chunklist = self._find_gibmacos_asset("BaseSystem.chunklist", os.path.dirname(original_bs_dmg), search_deep=False) - if original_bs_chunklist: - self._report_progress(f"Copying BaseSystem.chunklist...") - self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(coreservices_path_usb, "BaseSystem.chunklist")]) - self._run_command(["sudo", "cp", original_bs_chunklist, os.path.join(shared_support_path_usb_app, "BaseSystem.chunklist")]) + for f_name in ["BaseSystem.dmg", "BaseSystem.chunklist"]: + src_file = self._find_gibmacos_asset(f_name, product_folder_path, search_deep=True) + if src_file: self._run_command(["sudo", "cp", src_file, os.path.join(shared_support_path_usb_app, os.path.basename(src_file))]); self._run_command(["sudo", "cp", src_file, os.path.join(coreservices_path_usb, os.path.basename(src_file))]) + else: self._report_progress(f"Warning: {f_name} not found.") installinfo_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=True) - if installinfo_src: - self._report_progress(f"Copying InstallInfo.plist...") - self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]) - self._run_command(["sudo", "cp", installinfo_src, os.path.join(self.temp_usb_macos_target_mount, "InstallInfo.plist")]) + if installinfo_src: self._run_command(["sudo", "cp", installinfo_src, os.path.join(contents_path_usb, "Info.plist")]); self._run_command(["sudo", "cp", installinfo_src, os.path.join(usb_target_root, "InstallInfo.plist")]) + else: self._report_progress("Warning: InstallInfo.plist not found.") - packages_dir_usb_system = os.path.join(self.temp_usb_macos_target_mount, "System", "Installation", "Packages") - self._run_command(["sudo", "mkdir", "-p", packages_dir_usb_system]) - main_payload_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True) - if main_payload_src: - payload_basename = os.path.basename(main_payload_src) - self._report_progress(f"Copying main payload '{payload_basename}' to App SharedSupport and System Packages...") - self._run_command(["sudo", "cp", main_payload_src, os.path.join(shared_support_path_usb_app, payload_basename)]) - self._run_command(["sudo", "cp", main_payload_src, os.path.join(packages_dir_usb_system, payload_basename)]) + main_pkg_src = self._find_gibmacos_asset(["InstallAssistant.pkg", "InstallESD.dmg"], product_folder_path, search_deep=True) + if main_pkg_src: pkg_basename = os.path.basename(main_pkg_src); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(shared_support_path_usb_app, pkg_basename)]); self._run_command(["sudo", "cp", main_pkg_src, os.path.join(sys_install_pkgs_usb, pkg_basename)]) + else: self._report_progress("Warning: Main installer PKG/DMG not found.") - self._run_command(["sudo", "touch", os.path.join(coreservices_path_usb, "boot.efi")]) # Placeholder for bootability + diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=True) + if diag_src: self._run_command(["sudo", "cp", diag_src, os.path.join(shared_support_path_usb_app, "AppleDiagnostics.dmg")]) + + template_boot_efi = os.path.join(OC_TEMPLATE_DIR, "EFI", "BOOT", "BOOTx64.efi") + if os.path.exists(template_boot_efi) and os.path.getsize(template_boot_efi) > 0: self._run_command(["sudo", "cp", template_boot_efi, os.path.join(coreservices_path_usb, "boot.efi")]) + else: self._report_progress(f"Warning: Template BOOTx64.efi for installer's boot.efi not found or empty.") + + ia_product_info_path = os.path.join(usb_target_root, ".IAProductInfo") + ia_content_xml = "Product IDcom.apple.pkg.InstallAssistantProduct Path" + app_bundle_name + "/Contents/SharedSupport/InstallAssistant.pkg" + temp_ia_path = f"/tmp/temp_iaproductinfo_{pid}.plist" + with open(temp_ia_path, "w") as f: f.write(ia_content_xml) + self._run_command(["sudo", "cp", temp_ia_path, ia_product_info_path]) + if os.path.exists(temp_ia_path): os.remove(temp_ia_path) + + self._report_progress("macOS installer assets copied.") - # --- OpenCore EFI Setup --- self._report_progress("Setting up OpenCore EFI on ESP...") - self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev]) + self.mounted_usb_esp_path = f"/Volumes/EFI" # Default mount path for ESP + if not os.path.ismount(self.mounted_usb_esp_path): + self._run_command(["diskutil", "mount", "-mountPoint", self.temp_usb_esp_mount, esp_partition_dev]) + self.mounted_usb_esp_path = self.temp_usb_esp_mount + if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): self._create_minimal_efi_template(self.temp_efi_build_dir) else: self._run_command(["cp", "-a", f"{OC_TEMPLATE_DIR}/.", self.temp_efi_build_dir]) @@ -290,33 +304,27 @@ class USBWriterMacOS: if self.enhance_plist_enabled and enhance_config_plist and os.path.exists(temp_config_plist_path): self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...") - if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement processing complete.") + if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): self._report_progress("config.plist enhancement complete.") else: self._report_progress("config.plist enhancement call failed or had issues.") - self._report_progress(f"Copying final EFI folder to USB ESP ({self.temp_usb_esp_mount})...") - self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.temp_usb_esp_mount}/EFI/"]) - - self._report_progress(f"Blessing the installer volume: {self.temp_usb_macos_target_mount} with ESP {esp_partition_dev}") - # Correct bless command needs the folder containing boot.efi for the system being blessed, - # and the ESP mount point if different from system ESP. - # For installer, it's often /Volumes/Install macOS XXX/System/Library/CoreServices - bless_target_folder = os.path.join(self.temp_usb_macos_target_mount, "System", "Library", "CoreServices") - self._run_command(["sudo", "bless", "--folder", bless_target_folder, "--label", installer_vol_name, "--setBoot"], check=False) # SetBoot might be enough for OpenCore - # Alternative if ESP needs to be specified explicitly: - # self._run_command(["sudo", "bless", "--mount", self.temp_usb_macos_target_mount, "--setBoot", "--file", os.path.join(bless_target_folder, "boot.efi"), "--bootefi", os.path.join(self.temp_usb_esp_mount, "EFI", "BOOT", "BOOTx64.efi")], check=False) + self._report_progress(f"Copying final EFI folder to USB ESP ({self.mounted_usb_esp_path})...") + self._run_command(["sudo", "rsync", "-avh", "--delete", f"{self.temp_efi_build_dir}/EFI/", f"{self.mounted_usb_esp_path}/EFI/"]) + self._report_progress(f"Blessing the installer volume: {self.mounted_usb_macos_path}") + bless_target_folder = os.path.join(self.mounted_usb_macos_path, "System", "Library", "CoreServices") + self._run_command(["sudo", "bless", "--folder", bless_target_folder, "--label", installer_vol_name, "--setBoot"], check=False) self._report_progress("USB Installer creation process completed successfully.") return True except Exception as e: - self._report_progress(f"An error occurred during USB writing on macOS: {e}\n{traceback.format_exc()}") + self._report_progress(f"An error occurred during USB writing on macOS: {e}"); self._report_progress(traceback.format_exc()) return False finally: self._cleanup_temp_files_and_dirs() if __name__ == '__main__': import traceback - from constants import MACOS_VERSIONS # For testing _get_gibmacos_product_folder + from constants import MACOS_VERSIONS if platform.system() != "Darwin": print("This script is intended for macOS for standalone testing."); exit(1) print("USB Writer macOS Standalone Test - Installer Method") mock_download_dir = f"/tmp/temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) @@ -344,10 +352,11 @@ if __name__ == '__main__': print("\nAvailable external physical disks (use 'diskutil list external physical'):"); subprocess.run(["diskutil", "list", "external", "physical"], check=False) test_device = input("\nEnter target disk identifier (e.g., /dev/diskX). THIS DISK WILL BE WIPED: ") - if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) # No need to clean OC_TEMPLATE_DIR here + if not test_device or not test_device.startswith("/dev/disk"): print("Invalid disk."); shutil.rmtree(mock_download_dir, ignore_errors=True); exit(1) if input(f"Sure to wipe {test_device}? (yes/NO): ").lower() == 'yes': writer = USBWriterMacOS(test_device, mock_download_dir, print, True, target_version_cli) writer.format_and_write() else: print("Test cancelled.") shutil.rmtree(mock_download_dir, ignore_errors=True) + # Deliberately not cleaning OC_TEMPLATE_DIR in test, as it might be shared or pre-existing. print("Mock download dir cleaned up.") diff --git a/usb_writer_windows.py b/usb_writer_windows.py index 7ac0362..ecf3450 100644 --- a/usb_writer_windows.py +++ b/usb_writer_windows.py @@ -1,73 +1,92 @@ -# usb_writer_windows.py (Refining for installer workflow and guidance) +# usb_writer_windows.py (Refining EFI setup and manual step guidance) import subprocess import os import time import shutil import re -import glob # For _find_gibmacos_asset +import glob +import plistlib import traceback -import sys # For checking psutil import +import sys # Added for psutil check -# Try to import QMessageBox for the placeholder, otherwise use a mock for standalone test try: - from PyQt6.QtWidgets import QMessageBox # For user guidance + from PyQt6.QtWidgets import QMessageBox except ImportError: - class QMessageBox: # Mock for standalone testing + # Mock QMessageBox for standalone testing or if PyQt6 is not available + class QMessageBox: + Information = 1 # Dummy enum value + Warning = 2 # Dummy enum value + Question = 3 # Dummy enum value + YesRole = 0 # Dummy role + NoRole = 1 # Dummy role + @staticmethod - def information(*args): print(f"INFO (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'") + def information(parent, title, message, buttons=None, defaultButton=None): + print(f"INFO (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate a positive action if needed @staticmethod - def warning(*args): print(f"WARNING (QMessageBox mock): Title='{args[1]}', Message='{args[2]}'"); return QMessageBox - Yes = 1; No = 0; Cancel = 0 + def warning(parent, title, message, buttons=None, defaultButton=None): + print(f"WARNING (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate a positive action + @staticmethod + def critical(parent, title, message, buttons=None, defaultButton=None): + print(f"CRITICAL (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate a positive action + # Add other static methods if your code uses them, e.g. question + @staticmethod + def question(parent, title, message, buttons=None, defaultButton=None): + print(f"QUESTION (QMessageBox mock): Title='{title}', Message='{message}'") + return QMessageBox.Yes # Simulate 'Yes' for testing + + # Dummy button values if your code checks for specific button results + Yes = 0x00004000 + No = 0x00010000 + Cancel = 0x00400000 + try: from plist_modifier import enhance_config_plist except ImportError: - enhance_config_plist = None - print("Warning: plist_modifier.py not found. Plist enhancement feature will be disabled.") + print("Warning: plist_modifier not found. Enhancement will be skipped.") + def enhance_config_plist(plist_path, macos_version, progress_callback): + if progress_callback: + progress_callback("Skipping plist enhancement: plist_modifier not available.") + return False # Indicate failure or no action -OC_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "EFI_template_installer") +# This path needs to be correct relative to where usb_writer_windows.py is, or use an absolute path strategy +OC_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "EFI_template_installer") class USBWriterWindows: def __init__(self, device_id_str: str, macos_download_path: str, progress_callback=None, enhance_plist_enabled: bool = False, target_macos_version: str = ""): - # device_id_str is expected to be the disk number string from user, e.g., "1", "2" + self.device_id_str = device_id_str self.disk_number = "".join(filter(str.isdigit, device_id_str)) - if not self.disk_number: - # If device_id_str was like "disk 1", this will correctly get "1" - # If it was just "1", it's also fine. - # If it was invalid like "PhysicalDrive1", filter will get "1". - # This logic might need to be more robust if input format varies wildly. - pass # Allow it for now, diskpart will fail if self.disk_number is bad. - self.physical_drive_path = f"\\\\.\\PhysicalDrive{self.disk_number}" - self.macos_download_path = macos_download_path self.progress_callback = progress_callback self.enhance_plist_enabled = enhance_plist_enabled self.target_macos_version = target_macos_version pid = os.getpid() - self.temp_basesystem_hfs_path = f"temp_basesystem_{pid}.hfs" - self.temp_efi_build_dir = f"temp_efi_build_{pid}" - self.temp_dmg_extract_dir = f"temp_dmg_extract_{pid}" # For 7z extractions + # Use system temp for Windows more reliably + self.temp_dir_base = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_usb_temp_{pid}") + self.temp_basesystem_hfs_path = os.path.join(self.temp_dir_base, f"temp_basesystem_{pid}.hfs") + self.temp_efi_build_dir = os.path.join(self.temp_dir_base, f"temp_efi_build_{pid}") + self.temp_dmg_extract_dir = os.path.join(self.temp_dir_base, f"temp_dmg_extract_{pid}") - - self.temp_files_to_clean = [self.temp_basesystem_hfs_path] - self.temp_dirs_to_clean = [self.temp_efi_build_dir, self.temp_dmg_extract_dir] + self.temp_files_to_clean = [self.temp_basesystem_hfs_path] # Specific files outside temp_dir_base (if any) + self.temp_dirs_to_clean = [self.temp_dir_base] # Base temp dir for this instance self.assigned_efi_letter = None def _report_progress(self, message: str): if self.progress_callback: self.progress_callback(message) else: print(message) - def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None): + def _run_command(self, command: list[str] | str, check=True, capture_output=False, timeout=None, shell=False, working_dir=None, creationflags=0): self._report_progress(f"Executing: {command if isinstance(command, str) else ' '.join(command)}") try: - process = subprocess.run( - command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, - creationflags=subprocess.CREATE_NO_WINDOW - ) + process = subprocess.run(command, check=check, capture_output=capture_output, text=True, timeout=timeout, shell=shell, cwd=working_dir, creationflags=creationflags) if capture_output: if process.stdout and process.stdout.strip(): self._report_progress(f"STDOUT: {process.stdout.strip()}") if process.stderr and process.stderr.strip(): self._report_progress(f"STDERR: {process.stderr.strip()}") @@ -76,298 +95,529 @@ class USBWriterWindows: except subprocess.CalledProcessError as e: self._report_progress(f"Error executing (code {e.returncode}): {e.stderr or e.stdout or str(e)}"); raise except FileNotFoundError: self._report_progress(f"Error: Command '{command[0] if isinstance(command, list) else command.split()[0]}' not found."); raise - def _run_diskpart_script(self, script_content: str, capture_output_for_parse=False) -> str | None: - script_file_path = f"diskpart_script_{os.getpid()}.txt"; output_text = "" - with open(script_file_path, "w") as f: f.write(script_content) + script_file_path = os.path.join(self.temp_dir_base, f"diskpart_script_{os.getpid()}.txt") + os.makedirs(self.temp_dir_base, exist_ok=True) + output_text = None try: self._report_progress(f"Running diskpart script:\n{script_content}") - process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False) + with open(script_file_path, "w") as f: f.write(script_content) + # Use CREATE_NO_WINDOW for subprocess.run with diskpart + process = self._run_command(["diskpart", "/s", script_file_path], capture_output=True, check=False, creationflags=subprocess.CREATE_NO_WINDOW) output_text = (process.stdout or "") + "\n" + (process.stderr or "") - - success_indicators = ["DiskPart successfully", "successfully completed", "succeeded in creating", "successfully formatted", "successfully assigned"] - has_success_indicator = any(indicator in output_text for indicator in success_indicators) - has_error_indicator = "Virtual Disk Service error" in output_text or "DiskPart has encountered an error" in output_text - - if has_error_indicator: - self._report_progress(f"Diskpart script may have failed. Output:\n{output_text}") - elif not has_success_indicator and "There are no partitions on this disk to show" not in output_text : - self._report_progress(f"Diskpart script output does not clearly indicate success. Output:\n{output_text}") - if capture_output_for_parse: return output_text finally: - if os.path.exists(script_file_path): os.remove(script_file_path) - return output_text if capture_output_for_parse else None - + if os.path.exists(script_file_path): + try: os.remove(script_file_path) + except OSError as e: self._report_progress(f"Warning: Could not remove temp diskpart script {script_file_path}: {e}") + return None # Explicitly return None if not capturing for parse or if it fails before return def _cleanup_temp_files_and_dirs(self): - self._report_progress("Cleaning up...") + self._report_progress("Cleaning up temporary files and directories on Windows...") for f_path in self.temp_files_to_clean: if os.path.exists(f_path): try: os.remove(f_path) - except Exception as e: self._report_progress(f"Could not remove temp file {f_path}: {e}") - for d_path in self.temp_dirs_to_clean: + except OSError as e: self._report_progress(f"Error removing file {f_path}: {e}") + + for d_path in self.temp_dirs_to_clean: # self.temp_dir_base is the main one if os.path.exists(d_path): - try: shutil.rmtree(d_path, ignore_errors=True) - except Exception as e: self._report_progress(f"Could not remove temp dir {d_path}: {e}") + try: shutil.rmtree(d_path, ignore_errors=False) # Try with ignore_errors=False first + except OSError as e: + self._report_progress(f"Error removing dir {d_path}: {e}. Attempting force remove.") + try: shutil.rmtree(d_path, ignore_errors=True) # Fallback to ignore_errors=True + except OSError as e_force: self._report_progress(f"Force remove for dir {d_path} also failed: {e_force}") def _find_available_drive_letter(self) -> str | None: - import string; used_letters = set() + import string + used_letters = set() try: - if 'psutil' in sys.modules: # Check if psutil was imported by main app - partitions = sys.modules['psutil'].disk_partitions(all=True) + # Try to use psutil if available (e.g., when run from main_app.py) + if 'psutil' in sys.modules: + import psutil # Ensure it's imported here if check passes + partitions = psutil.disk_partitions(all=True) for p in partitions: - if p.mountpoint and len(p.mountpoint) >= 2 and p.mountpoint[1] == ':': # Check for "X:" + if p.mountpoint and len(p.mountpoint) == 2 and p.mountpoint[1] == ':': used_letters.add(p.mountpoint[0].upper()) - except Exception as e: - self._report_progress(f"Could not list used drive letters with psutil: {e}. Will try common letters.") + else: # Fallback if psutil is not available (e.g. pure standalone script) + self._report_progress("psutil not available, using limited drive letter detection.") + # Basic check, might not be exhaustive + for letter in string.ascii_uppercase[3:]: # D onwards + if os.path.exists(f"{letter}:\\"): + used_letters.add(letter) + except Exception as e: + self._report_progress(f"Error detecting used drive letters: {e}. Proceeding with caution.") + + # Prefer letters from S onwards, less likely to conflict with user drives for letter in "STUVWXYZGHIJKLMNOPQR": - if letter not in used_letters and letter > 'D': # Avoid A, B, C, D + if letter not in used_letters and letter > 'C': # Ensure it's not A, B, C return letter return None def check_dependencies(self): self._report_progress("Checking dependencies (diskpart, robocopy, 7z, dd for Windows [manual check])...") dependencies = ["diskpart", "robocopy", "7z"] - missing_deps = [dep for dep in dependencies if not shutil.which(dep)] - if missing_deps: - msg = f"Missing dependencies: {', '.join(missing_deps)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip CLI) needs to be installed and in PATH (for extracting installer assets)." - self._report_progress(msg); raise RuntimeError(msg) - self._report_progress("Please ensure a 'dd for Windows' utility is installed and in your PATH for writing the main macOS BaseSystem image.") + missing = [dep for dep in dependencies if not shutil.which(dep)] + if missing: + msg = f"Missing dependencies: {', '.join(missing)}. `diskpart` & `robocopy` should be standard. `7z.exe` (7-Zip) needs to be installed and its directory added to the system PATH." + self._report_progress(msg) + raise RuntimeError(msg) + self._report_progress("Please ensure a 'dd for Windows' utility (e.g., from SUSE, Cygwin, or http://www.chrysocome.net/dd) is installed and accessible from your PATH for writing the main macOS BaseSystem image.") return True - def _find_gibmacos_asset(self, asset_patterns: list[str] | str, product_folder_path: str | None = None, search_deep=True) -> str | None: - if isinstance(asset_patterns, str): asset_patterns = [asset_patterns] - search_base = product_folder_path or self.macos_download_path - self._report_progress(f"Searching for {asset_patterns} in {search_base} and subdirectories...") - for pattern in asset_patterns: - common_subdirs_for_pattern = ["", "SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/SharedSupport", f"Install macOS {self.target_macos_version}.app/Contents/Resources"] - for sub_dir_pattern in common_subdirs_for_pattern: - current_search_base = os.path.join(search_base, sub_dir_pattern) - glob_pattern = os.path.join(glob.escape(current_search_base), pattern) - found_files = glob.glob(glob_pattern, recursive=False) - if found_files: - found_files.sort(key=os.path.getsize, reverse=True) - self._report_progress(f"Found '{pattern}' at: {found_files[0]} (in {current_search_base})") - return found_files[0] - if search_deep: - deep_search_pattern = os.path.join(glob.escape(search_base), "**", pattern) - found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=len) - if found_files_deep: - self._report_progress(f"Found '{pattern}' via deep search at: {found_files_deep[0]}") - return found_files_deep[0] - self._report_progress(f"Warning: Asset matching patterns '{asset_patterns}' not found in {search_base}.") + def _find_gibmacos_asset(self, asset_name: str, product_folder_path: str | None = None, search_deep=True) -> str | None: + search_locations = [] + if product_folder_path and os.path.isdir(product_folder_path): + search_locations.extend([product_folder_path, os.path.join(product_folder_path, "SharedSupport")]) + + # Also search directly in macos_download_path and a potential "macOS Install Data" subdirectory + search_locations.extend([self.macos_download_path, os.path.join(self.macos_download_path, "macOS Install Data")]) + + # If a version-specific folder exists at the root of macos_download_path (less common for gibMacOS structure) + if os.path.isdir(self.macos_download_path): + for item in os.listdir(self.macos_download_path): + item_path = os.path.join(self.macos_download_path, item) + if os.path.isdir(item_path) and self.target_macos_version.lower() in item.lower(): + search_locations.append(item_path) + search_locations.append(os.path.join(item_path, "SharedSupport")) + # Assuming first match is good enough for this heuristic + break + + # Deduplicate search locations while preserving order (Python 3.7+) + search_locations = list(dict.fromkeys(search_locations)) + + for loc in search_locations: + if not os.path.isdir(loc): continue + + path = os.path.join(loc, asset_name) + if os.path.exists(path): + self._report_progress(f"Found '{asset_name}' at: {path}") + return path + + # Case-insensitive glob as fallback for direct name match + # Create a pattern like "[bB][aA][sS][eE][sS][yY][sS][tT][eE][mM].[dD][mM][gG]" + pattern_parts = [f"[{c.lower()}{c.upper()}]" if c.isalpha() else re.escape(c) for c in asset_name] + insensitive_glob_pattern = "".join(pattern_parts) + + found_files = glob.glob(os.path.join(loc, insensitive_glob_pattern), recursive=False) + if found_files: + self._report_progress(f"Found '{asset_name}' via case-insensitive glob at: {found_files[0]}") + return found_files[0] + + if search_deep: + self._report_progress(f"Asset '{asset_name}' not found in primary locations, starting deep search in {self.macos_download_path}...") + deep_search_pattern = os.path.join(self.macos_download_path, "**", asset_name) + # Sort by length to prefer shallower paths, then alphabetically + found_files_deep = sorted(glob.glob(deep_search_pattern, recursive=True), key=lambda p: (len(os.path.dirname(p)), p)) + if found_files_deep: + self._report_progress(f"Found '{asset_name}' via deep search at: {found_files_deep[0]}") + return found_files_deep[0] + + self._report_progress(f"Warning: Asset '{asset_name}' not found.") return None def _get_gibmacos_product_folder(self) -> str | None: - from constants import MACOS_VERSIONS + # constants.py should be in the same directory or Python path + try: from constants import MACOS_VERSIONS + except ImportError: MACOS_VERSIONS = {} ; self._report_progress("Warning: MACOS_VERSIONS from constants.py not loaded.") + + # Standard gibMacOS download structure: macOS Downloads/publicrelease/012-34567 - macOS Sonoma 14.0 base_path = os.path.join(self.macos_download_path, "macOS Downloads", "publicrelease") - if not os.path.isdir(base_path): base_path = self.macos_download_path + if not os.path.isdir(base_path): + # Fallback if "macOS Downloads/publicrelease" is not present, use macos_download_path directly + base_path = self.macos_download_path + if os.path.isdir(base_path): + potential_folders = [] for item in os.listdir(base_path): item_path = os.path.join(base_path, item) - version_tag = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version).lower() - if os.path.isdir(item_path) and (self.target_macos_version.lower() in item.lower() or version_tag in item.lower()): - self._report_progress(f"Identified gibMacOS product folder: {item_path}"); return item_path - self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}"); return self.macos_download_path + # Check if it's a directory and matches target_macos_version (name or tag) + version_tag_from_constants = MACOS_VERSIONS.get(self.target_macos_version, self.target_macos_version.lower().replace(" ", "")) + if os.path.isdir(item_path) and \ + (self.target_macos_version.lower() in item.lower() or \ + version_tag_from_constants.lower() in item.lower().replace(" ", "")): + potential_folders.append(item_path) + if potential_folders: + # Sort by length (prefer shorter, more direct matches) or other heuristics if needed + best_match = min(potential_folders, key=len) + self._report_progress(f"Identified gibMacOS product folder: {best_match}") + return best_match + + self._report_progress(f"Could not identify a specific product folder for '{self.target_macos_version}'. Using general download path: {self.macos_download_path}") + return self.macos_download_path # Fallback to the root download path def _extract_hfs_from_dmg_or_pkg(self, dmg_or_pkg_path: str, output_hfs_path: str) -> bool: - os.makedirs(self.temp_dmg_extract_dir, exist_ok=True); current_target = dmg_or_pkg_path + temp_extract_dir = self.temp_dmg_extract_dir + os.makedirs(temp_extract_dir, exist_ok=True) + current_target = dmg_or_pkg_path try: - if dmg_or_pkg_path.endswith(".pkg"): - self._report_progress(f"Extracting DMG from PKG {current_target}..."); self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{self.temp_dmg_extract_dir}"], check=True) - dmgs_in_pkg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.dmg")); - if not dmgs_in_pkg: raise RuntimeError("No DMG found in PKG.") - current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) or dmgs_in_pkg[0] - if not current_target: raise RuntimeError("Could not determine primary DMG in PKG.") + if not os.path.exists(current_target): + self._report_progress(f"Error: Input file for HFS extraction does not exist: {current_target}"); return False + + # Step 1: If it's a PKG, extract DMGs from it. + if dmg_or_pkg_path.lower().endswith(".pkg"): + self._report_progress(f"Extracting DMG(s) from PKG: {current_target} using 7z...") + # Using 'e' to extract flat, '-txar' for PKG/XAR format. + self._run_command(["7z", "e", "-txar", current_target, "*.dmg", f"-o{temp_extract_dir}", "-y"], check=True) + dmgs_in_pkg = glob.glob(os.path.join(temp_extract_dir, "*.dmg")) + if not dmgs_in_pkg: self._report_progress(f"No DMG files found after extracting PKG: {current_target}"); return False + # Select the largest DMG, assuming it's the main one. + current_target = max(dmgs_in_pkg, key=os.path.getsize, default=None) + if not current_target: self._report_progress("Failed to select a DMG from PKG contents."); return False self._report_progress(f"Using DMG from PKG: {current_target}") - if not current_target or not current_target.endswith(".dmg"): raise RuntimeError(f"Not a valid DMG: {current_target}") + + # Step 2: Ensure we have a DMG file. + if not current_target or not current_target.lower().endswith(".dmg"): + self._report_progress(f"Not a valid DMG file for HFS extraction: {current_target}"); return False basesystem_dmg_to_process = current_target + # Step 3: If the DMG is not BaseSystem.dmg, try to extract BaseSystem.dmg from it. + # This handles cases like SharedSupport.dmg containing BaseSystem.dmg. if "basesystem.dmg" not in os.path.basename(current_target).lower(): - self._report_progress(f"Extracting BaseSystem.dmg from {current_target}..."); self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{self.temp_dmg_extract_dir}"], check=True) - found_bs_dmg = glob.glob(os.path.join(self.temp_dmg_extract_dir, "**", "*BaseSystem.dmg"), recursive=True) - if not found_bs_dmg: raise RuntimeError(f"Could not extract BaseSystem.dmg from {current_target}") - basesystem_dmg_to_process = found_bs_dmg[0] + self._report_progress(f"Extracting BaseSystem.dmg from container DMG: {current_target} using 7z...") + # Extract recursively, looking for any path that includes BaseSystem.dmg + self._run_command(["7z", "e", current_target, "*/BaseSystem.dmg", "-r", f"-o{temp_extract_dir}", "-y"], check=True) + found_bs_dmg_list = glob.glob(os.path.join(temp_extract_dir, "**", "*BaseSystem.dmg"), recursive=True) + if not found_bs_dmg_list: self._report_progress(f"No BaseSystem.dmg found within {current_target}"); return False + basesystem_dmg_to_process = max(found_bs_dmg_list, key=os.path.getsize, default=None) # Largest if multiple + if not basesystem_dmg_to_process: self._report_progress("Failed to select BaseSystem.dmg from container."); return False + self._report_progress(f"Processing extracted BaseSystem.dmg: {basesystem_dmg_to_process}") - self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process}..."); self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{self.temp_dmg_extract_dir}"], check=True) - hfs_files = glob.glob(os.path.join(self.temp_dmg_extract_dir, "*.hfs")); - if not hfs_files: - self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*", f"-o{self.temp_dmg_extract_dir}"], check=True) # Try extracting all files - hfs_files = [os.path.join(self.temp_dmg_extract_dir, f) for f in os.listdir(self.temp_dmg_extract_dir) if not f.lower().endswith((".xml",".chunklist",".plist")) and os.path.isfile(os.path.join(self.temp_dmg_extract_dir,f)) and os.path.getsize(os.path.join(self.temp_dmg_extract_dir,f)) > 100*1024*1024] # Min 100MB HFS + # Step 4: Extract HFS partition image from BaseSystem.dmg. + self._report_progress(f"Extracting HFS+ partition image from {basesystem_dmg_to_process} using 7z...") + # Using 'e' to extract flat, '-tdmg' for DMG format. Looking for '*.hfs' or specific partition files. + # Common HFS file names inside BaseSystem.dmg are like '2.hfs' or similar. + # Sometimes they don't have .hfs extension, 7z might list them by index. + # We will try to extract any .hfs file. + self._run_command(["7z", "e", "-tdmg", basesystem_dmg_to_process, "*.hfs", f"-o{temp_extract_dir}", "-y"], check=True) + hfs_files = glob.glob(os.path.join(temp_extract_dir, "*.hfs")) - if not hfs_files: raise RuntimeError(f"No suitable .hfs image found after extracting {basesystem_dmg_to_process}") - final_hfs_file = max(hfs_files, key=os.path.getsize); self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}"); shutil.move(final_hfs_file, output_hfs_path); return True - except Exception as e: self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False - finally: - if os.path.exists(self.temp_dmg_extract_dir): shutil.rmtree(self.temp_dmg_extract_dir, ignore_errors=True) + if not hfs_files: # If no .hfs, try extracting by common partition indices if 7z supports listing them for DMG + self._report_progress("No direct '*.hfs' found. Attempting extraction of common HFS partition by index (e.g., '2', '3')...") + # This is more complex as 7z CLI might not easily allow extracting by index directly without listing first. + # For now, we rely on .hfs existing. If this fails, user might need to extract manually with 7z GUI. + # A more robust solution would involve listing contents and then extracting the correct file. + self._report_progress("Extraction by index is not implemented. Please ensure BaseSystem.dmg contains a directly extractable .hfs file.") + return False - def _create_minimal_efi_template(self, efi_dir_path): - self._report_progress(f"Minimal EFI template directory not found or empty. Creating basic structure at {efi_dir_path}") - oc_dir = os.path.join(efi_dir_path, "EFI", "OC"); os.makedirs(os.path.join(efi_dir_path, "EFI", "BOOT"), exist_ok=True); os.makedirs(oc_dir, exist_ok=True) - for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True) - with open(os.path.join(efi_dir_path, "EFI", "BOOT", "BOOTx64.efi"), "w") as f: f.write("") - with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("") - basic_config_content = {"#Comment": "Basic config template by Skyscope", "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, "PlatformInfo": {"Generic":{"MLB":"CHANGE_ME_MLB", "SystemSerialNumber":"CHANGE_ME_SERIAL", "SystemUUID":"CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}} + if not hfs_files: self._report_progress(f"No HFS files found after extracting DMG: {basesystem_dmg_to_process}"); return False + + final_hfs_file = max(hfs_files, key=os.path.getsize, default=None) # Largest HFS file + if not final_hfs_file: self._report_progress("Failed to select HFS file."); return False + + self._report_progress(f"Found HFS+ image: {final_hfs_file}. Moving to {output_hfs_path}") + shutil.move(final_hfs_file, output_hfs_path) + return True + except Exception as e: + self._report_progress(f"Error during HFS extraction: {e}\n{traceback.format_exc()}"); return False + + def _create_minimal_efi_template_content(self, efi_dir_path_root): + self._report_progress(f"Minimal EFI template directory '{OC_TEMPLATE_DIR}' not found or is empty. Creating basic structure at {efi_dir_path_root}") + efi_path = os.path.join(efi_dir_path_root, "EFI") + oc_dir = os.path.join(efi_path, "OC") + os.makedirs(os.path.join(efi_path, "BOOT"), exist_ok=True) + os.makedirs(oc_dir, exist_ok=True) + for sub_dir in ["Drivers", "Kexts", "ACPI", "Tools", "Resources"]: + os.makedirs(os.path.join(oc_dir, sub_dir), exist_ok=True) + + # Create dummy BOOTx64.efi and OpenCore.efi + with open(os.path.join(efi_path, "BOOT", "BOOTx64.efi"), "w") as f: f.write("Minimal Boot") + with open(os.path.join(oc_dir, "OpenCore.efi"), "w") as f: f.write("Minimal OC") + + # Create a very basic config.plist + basic_config = { + "#WARNING": "This is a minimal config.plist. Replace with a full one for booting macOS!", + "Misc": {"Security": {"ScanPolicy": 0, "SecureBootModel": "Disabled"}}, + "PlatformInfo": {"Generic": {"MLB": "CHANGE_ME_MLB", "SystemSerialNumber": "CHANGE_ME_SERIAL", "SystemUUID": "CHANGE_ME_UUID", "ROM": b"\x00\x00\x00\x00\x00\x00"}}, + "NVRAM": {"Add": {"4D1EDE05-38C7-4A6A-9CC6-4BCCA8B38C14": {"DefaultBackgroundColor": "00000000", "UIScale": "01"}}}, # Basic NVRAM + "UEFI": {"Drivers": ["OpenRuntime.efi"], "Input": {"KeySupport": True}} # Example + } + config_plist_path = os.path.join(oc_dir, "config.plist") try: - with open(os.path.join(oc_dir, "config.plist"), 'wb') as f: plistlib.dump(basic_config_content, f, fmt=plistlib.PlistFormat.XML) - self._report_progress("Created basic placeholder config.plist.") - except Exception as e: self._report_progress(f"Could not create basic config.plist: {e}") + with open(config_plist_path, 'wb') as fp: + plistlib.dump(basic_config, fp, fmt=plistlib.PlistFormat.XML) + self._report_progress(f"Created minimal config.plist at {config_plist_path}") + except Exception as e: + self._report_progress(f"Error creating minimal config.plist: {e}") def format_and_write(self) -> bool: try: self.check_dependencies() - self._cleanup_temp_files_and_dirs() - os.makedirs(self.temp_efi_build_dir, exist_ok=True) + if os.path.exists(self.temp_dir_base): + self._report_progress(f"Cleaning up existing temp base directory: {self.temp_dir_base}") + shutil.rmtree(self.temp_dir_base, ignore_errors=True) + os.makedirs(self.temp_dir_base, exist_ok=True) + os.makedirs(self.temp_efi_build_dir, exist_ok=True) # For building EFI contents before copy + os.makedirs(self.temp_dmg_extract_dir, exist_ok=True) # For 7z extractions self._report_progress(f"WARNING: ALL DATA ON DISK {self.disk_number} ({self.physical_drive_path}) WILL BE ERASED!") + # Optional: Add a QMessageBox.question here for final confirmation in GUI mode self.assigned_efi_letter = self._find_available_drive_letter() if not self.assigned_efi_letter: raise RuntimeError("Could not find an available drive letter for EFI.") - self._report_progress(f"Will assign letter {self.assigned_efi_letter}: to EFI partition.") + self._report_progress(f"Will attempt to assign letter {self.assigned_efi_letter}: to EFI partition.") installer_vol_label = f"Install macOS {self.target_macos_version}" + # Ensure label for diskpart is max 32 chars for FAT32. "Install macOS Monterey" is 23 chars. diskpart_script_part1 = f"select disk {self.disk_number}\nclean\nconvert gpt\n" - diskpart_script_part1 += f"create partition efi size=550 label=\"EFI\"\nformat fs=fat32 quick\nassign letter={self.assigned_efi_letter}\n" + # Create EFI (ESP) partition, 550MB is generous and common + diskpart_script_part1 += f"create partition efi size=550\nformat fs=fat32 quick label=EFI\nassign letter={self.assigned_efi_letter}\n" + # Create main macOS partition (HFS+). Let diskpart use remaining space. + # AF00 is Apple HFS+ type GUID. For APFS, it's 7C3457EF-0000-11AA-AA11-00306543ECAC + # We create as HFS+ because BaseSystem is HFS+. Installer will convert if needed. diskpart_script_part1 += f"create partition primary label=\"{installer_vol_label[:31]}\" id=AF00\nexit\n" - self._run_diskpart_script(diskpart_script_part1) - time.sleep(5) - macos_partition_offset_str = "Offset not determined by diskpart" - macos_partition_number_str = "2 (assumed)" + self._run_diskpart_script(diskpart_script_part1) + self._report_progress("Disk partitioning complete. Waiting for volumes to settle...") + time.sleep(5) # Give Windows time to recognize new partitions + + macos_partition_number_str = "2 (typically)"; macos_partition_offset_str = "Offset not automatically determined for Windows dd" try: - diskpart_script_detail = f"select disk {self.disk_number}\nselect partition 2\ndetail partition\nexit\n" + # Attempt to get partition details. This is informational. + diskpart_script_detail = f"select disk {self.disk_number}\nlist partition\nexit\n" detail_output = self._run_diskpart_script(diskpart_script_detail, capture_output_for_parse=True) if detail_output: - self._report_progress(f"Detail Partition Output:\n{detail_output}") - offset_match = re.search(r"Offset in Bytes\s*:\s*(\d+)", detail_output, re.IGNORECASE) - if offset_match: macos_partition_offset_str = f"{offset_match.group(1)} bytes ({int(offset_match.group(1)) // (1024*1024)} MiB)" - num_match = re.search(r"Partition\s+(\d+)\s*\n\s*Type", detail_output, re.IGNORECASE | re.MULTILINE) - if num_match: - macos_partition_number_str = num_match.group(1) - self._report_progress(f"Determined macOS partition number: {macos_partition_number_str}") + # Try to find Partition 2, assuming it's our target HFS+ partition + part_match = re.search(r"Partition 2\s+Primary\s+\d+\s+[GMK]B\s+(\d+)\s+[GMK]B", detail_output, re.IGNORECASE) + if part_match: + macos_partition_offset_str = f"{part_match.group(1)} MB (approx. from start of disk for Partition 2)" + else: # Fallback if specific regex fails + self._report_progress("Could not parse partition 2 offset, using generic message.") except Exception as e: - self._report_progress(f"Could not get partition details from diskpart: {e}") + self._report_progress(f"Could not get detailed partition info from diskpart: {e}") + # --- OpenCore EFI Setup --- self._report_progress("Setting up OpenCore EFI on ESP...") if not os.path.isdir(OC_TEMPLATE_DIR) or not os.listdir(OC_TEMPLATE_DIR): - self._create_minimal_efi_template(self.temp_efi_build_dir) + self._report_progress(f"EFI_template_installer at '{OC_TEMPLATE_DIR}' is missing or empty.") + self._create_minimal_efi_template_content(self.temp_efi_build_dir) # Create in temp_efi_build_dir else: - self._report_progress(f"Copying OpenCore EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}") - if os.path.exists(self.temp_efi_build_dir): shutil.rmtree(self.temp_efi_build_dir) + self._report_progress(f"Copying EFI template from {OC_TEMPLATE_DIR} to {self.temp_efi_build_dir}") shutil.copytree(OC_TEMPLATE_DIR, self.temp_efi_build_dir, dirs_exist_ok=True) temp_config_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config.plist") if not os.path.exists(temp_config_plist_path): - template_plist_src = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") - if os.path.exists(template_plist_src): shutil.copy2(template_plist_src, temp_config_plist_path) - else: self._create_minimal_efi_template(self.temp_efi_build_dir) # Fallback + template_plist_path = os.path.join(self.temp_efi_build_dir, "EFI", "OC", "config-template.plist") + if os.path.exists(template_plist_path): + self._report_progress(f"Using template config: {template_plist_path}") + shutil.copy2(template_plist_path, temp_config_plist_path) + else: + self._report_progress("No config.plist or config-template.plist found in EFI template. Creating a minimal one.") + plistlib.dump({"#Comment": "Minimal config by Skyscope - REPLACE ME", "PlatformInfo": {"Generic": {"MLB": "CHANGE_ME"}}}, + open(temp_config_plist_path, 'wb'), fmt=plistlib.PlistFormat.XML) - if self.enhance_plist_enabled and enhance_config_plist: - self._report_progress("Attempting to enhance config.plist (note: hardware detection is Linux-only)...") + if self.enhance_plist_enabled and enhance_config_plist: # Check if function exists + self._report_progress("Attempting to enhance config.plist (note: hardware detection for enhancement is primarily Linux-based)...") if enhance_config_plist(temp_config_plist_path, self.target_macos_version, self._report_progress): - self._report_progress("config.plist enhancement processing complete.") - else: self._report_progress("config.plist enhancement call failed or had issues.") + self._report_progress("config.plist enhancement process complete.") + else: + self._report_progress("config.plist enhancement process failed or had issues (this is expected on Windows for hardware-specifics).") target_efi_on_usb_root = f"{self.assigned_efi_letter}:\\" - time.sleep(2) # Allow drive letter to be fully active - if not os.path.exists(target_efi_on_usb_root): raise RuntimeError(f"EFI partition {self.assigned_efi_letter}: not accessible.") + # Ensure the assigned drive letter is actually available before robocopy + if not os.path.exists(target_efi_on_usb_root): + time.sleep(3) # Extra wait + if not os.path.exists(target_efi_on_usb_root): + raise RuntimeError(f"EFI partition {target_efi_on_usb_root} not accessible after formatting and assignment.") - self._report_progress(f"Copying final EFI folder to USB ESP ({target_efi_on_usb_root})...") - self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), target_efi_on_usb_root + "EFI", "/E", "/S", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/XO"], check=True) + self._report_progress(f"Copying final EFI folder from {os.path.join(self.temp_efi_build_dir, 'EFI')} to USB ESP ({target_efi_on_usb_root}EFI)...") + # Using robocopy: /E for subdirs (incl. empty), /S for non-empty, /NFL no file list, /NDL no dir list, /NJH no job header, /NJS no job summary, /NC no class, /NS no size, /NP no progress + # /MT:8 for multithreading (default is 8, can be 1-128) + self._run_command(["robocopy", os.path.join(self.temp_efi_build_dir, "EFI"), os.path.join(target_efi_on_usb_root, "EFI"), "/E", "/NFL", "/NDL", "/NJH", "/NJS", "/NC", "/NS", "/NP", "/MT:8", "/R:3", "/W:5"], check=True) self._report_progress(f"EFI setup complete on {target_efi_on_usb_root}") # --- Prepare BaseSystem HFS Image --- - self._report_progress("Locating BaseSystem image from downloaded assets...") + self._report_progress("Locating BaseSystem image (DMG or PKG containing it) from downloaded assets...") product_folder_path = self._get_gibmacos_product_folder() - source_for_hfs_extraction = self._find_gibmacos_asset(["BaseSystem.dmg", "InstallESD.dmg", "SharedSupport.dmg", "InstallAssistant.pkg"], product_folder_path, "BaseSystem.dmg (or source like InstallESD.dmg/SharedSupport.dmg/InstallAssistant.pkg)") - if not source_for_hfs_extraction: raise RuntimeError("Could not find BaseSystem.dmg, InstallESD.dmg, SharedSupport.dmg or InstallAssistant.pkg.") + basesystem_source_dmg_or_pkg = ( + self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path) or + self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path) or # Common for newer macOS + self._find_gibmacos_asset("SharedSupport.dmg", product_folder_path) # Older fallback + ) + if not basesystem_source_dmg_or_pkg: + # Last resort: search for any large PKG file as it might be the installer + if product_folder_path: + pkgs = glob.glob(os.path.join(product_folder_path, "*.pkg")) + glob.glob(os.path.join(product_folder_path, "SharedSupport", "*.pkg")) + if pkgs: basesystem_source_dmg_or_pkg = max(pkgs, key=os.path.getsize, default=None) + if not basesystem_source_dmg_or_pkg: + raise RuntimeError("Could not find BaseSystem.dmg, InstallAssistant.pkg, or SharedSupport.dmg in expected locations.") - if not self._extract_hfs_from_dmg_or_pkg(source_for_hfs_extraction, self.temp_basesystem_hfs_path): - raise RuntimeError("Failed to extract HFS+ image from BaseSystem assets.") + self._report_progress(f"Selected source for HFS extraction: {basesystem_source_dmg_or_pkg}") + if not self._extract_hfs_from_dmg_or_pkg(basesystem_source_dmg_or_pkg, self.temp_basesystem_hfs_path): + raise RuntimeError(f"Failed to extract HFS+ image from '{basesystem_source_dmg_or_pkg}'. Check 7z output above.") - abs_hfs_path = os.path.abspath(self.temp_basesystem_hfs_path) - abs_download_path = os.path.abspath(self.macos_download_path) + # --- Guidance for Manual Steps --- + abs_hfs_path_win = os.path.abspath(self.temp_basesystem_hfs_path).replace("/", "\\") + abs_download_path_win = os.path.abspath(self.macos_download_path).replace("/", "\\") + physical_drive_path_win = self.physical_drive_path # Already has escaped backslashes for \\.\ + + # Try to find specific assets for better guidance + install_info_plist_src = self._find_gibmacos_asset("InstallInfo.plist", product_folder_path, search_deep=False) or "InstallInfo.plist (find in product folder)" + basesystem_dmg_src = self._find_gibmacos_asset("BaseSystem.dmg", product_folder_path, search_deep=False) or "BaseSystem.dmg" + basesystem_chunklist_src = self._find_gibmacos_asset("BaseSystem.chunklist", product_folder_path, search_deep=False) or "BaseSystem.chunklist" + main_installer_pkg_src = self._find_gibmacos_asset("InstallAssistant.pkg", product_folder_path, search_deep=False) or \ + self._find_gibmacos_asset("InstallESD.dmg", product_folder_path, search_deep=False) or \ + "InstallAssistant.pkg OR InstallESD.dmg (main installer package)" + apple_diag_src = self._find_gibmacos_asset("AppleDiagnostics.dmg", product_folder_path, search_deep=False) or "AppleDiagnostics.dmg (if present)" - # Key assets to mention for manual copy by user - assets_to_copy_manually = [ - "InstallInfo.plist (to root of macOS partition)", - "BaseSystem.dmg (to System/Library/CoreServices/ on macOS partition)", - "BaseSystem.chunklist (to System/Library/CoreServices/ on macOS partition)", - "InstallAssistant.pkg or InstallESD.dmg (to System/Installation/Packages/ on macOS partition)", - "AppleDiagnostics.dmg (if present, to a temporary location then to .app/Contents/SharedSupport/ if making full app structure)" - ] - assets_list_str = "\n - ".join(assets_to_copy_manually) guidance_message = ( - f"EFI setup complete on drive {self.assigned_efi_letter}:.\n" - f"BaseSystem HFS image for macOS installer extracted to: '{abs_hfs_path}'.\n\n" - f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (Partition {macos_partition_number_str} on Disk {self.disk_number}):\n" - f"1. Write BaseSystem Image: Open Command Prompt or PowerShell AS ADMINISTRATOR.\n" - f" Use a 'dd for Windows' utility. Example (VERIFY SYNTAX FOR YOUR DD TOOL & TARGETS!):\n" - f" `dd if=\"{abs_hfs_path}\" of={self.physical_drive_path} --target-partition {macos_partition_number_str} bs=4M --progress` (Conceptual)\n" - f" (Offset for partition {macos_partition_number_str} on Disk {self.disk_number} is approx. {macos_partition_offset_str})\n\n" - f"2. Copy Other Installer Files: After writing BaseSystem, the 'Install macOS {self.target_macos_version}' partition on USB needs other files from your download path: '{abs_download_path}'.\n" - f" This requires a tool that can write to HFS+ partitions from Windows (e.g., TransMac, Paragon HFS+ for Windows), or doing this step on a macOS/Linux system.\n" - f" Key files to find in '{abs_download_path}' and copy to the HFS+ partition:\n - {assets_list_str}\n" - f" (You might need to create directories like 'System/Library/CoreServices/' and 'System/Installation/Packages/' on the HFS+ partition first using your HFS+ tool).\n\n" - "Without these additional files, the USB might only boot to an internet recovery mode (if network & EFI are correct)." - ) - self._report_progress(f"GUIDANCE:\n{guidance_message}") - QMessageBox.information(None, "Manual Steps Required for Windows USB", guidance_message) + f"AUTOMATED EFI SETUP COMPLETE on drive {self.assigned_efi_letter}: (USB partition 1).\n" + f"TEMPORARY BaseSystem HFS image prepared at: '{abs_hfs_path_win}'.\n\n" + f"MANUAL STEPS REQUIRED FOR MAIN macOS PARTITION (USB partition {macos_partition_number_str} - '{installer_vol_label}'):\n" + f"TARGET DISK: Disk {self.disk_number} ({physical_drive_path_win})\n" + f"TARGET PARTITION FOR HFS+ CONTENT: Partition {macos_partition_number_str} (Offset from disk start: {macos_partition_offset_str}).\n\n" - self._report_progress("Windows USB installer preparation (EFI automated, macOS content manual steps provided).") + f"1. WRITE BaseSystem IMAGE:\n" + f" You MUST use a 'dd for Windows' utility. Open Command Prompt or PowerShell AS ADMINISTRATOR.\n" + f" Example command (VERIFY SYNTAX & TARGETS for YOUR dd tool! Incorrect use can WIPE OTHER DRIVES!):\n" + f" `dd if=\"{abs_hfs_path_win}\" of={physical_drive_path_win} bs=8M --progress` (if targeting whole disk with offset for partition 2)\n" + f" OR (if your dd supports writing directly to a partition by its number/offset, less common for \\\\.\\PhysicalDrive targets):\n" + f" `dd if=\"{abs_hfs_path_win}\" of=\\\\?\\Volume{{GUID_OF_PARTITION_2}}\ bs=8M --progress` (more complex to get GUID)\n" + f" It's often SAFER to write to the whole physical drive path ({physical_drive_path_win}) if your `dd` version calculates offsets correctly or if you specify the exact starting sector/byte offset for partition 2.\n" + f" The BaseSystem HFS image is approx. {os.path.getsize(self.temp_basesystem_hfs_path)/(1024*1024):.2f} MB.\n\n" + + f"2. COPY OTHER INSTALLER FILES (CRITICAL FOR OFFLINE INSTALLER):\n" + f" After `dd`-ing BaseSystem.hfs, the '{installer_vol_label}' partition on the USB needs more files from your download path: '{abs_download_path_win}'.\n" + f" This requires a tool that can WRITE to HFS+ partitions from Windows (e.g., TransMac, Paragon HFS+ for Windows, HFSExplorer with write capabilities if any), OR perform this step on macOS/Linux.\n\n" + f" KEY FILES/FOLDERS TO COPY from '{abs_download_path_win}' (likely within a subfolder named like '{os.path.basename(product_folder_path if product_folder_path else '')}') to the ROOT of the '{installer_vol_label}' USB partition:\n" + f" a. Create folder: `Install macOS {self.target_macos_version}.app` (this is a directory)\n" + f" b. Copy '{os.path.basename(install_info_plist_src)}' to the root of '{installer_vol_label}' partition.\n" + f" c. Copy '{os.path.basename(basesystem_dmg_src)}' AND '{os.path.basename(basesystem_chunklist_src)}' into: `System/Library/CoreServices/` (on '{installer_vol_label}')\n" + f" d. Copy '{os.path.basename(main_installer_pkg_src)}' into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/`\n" + f" (Alternatively, for older macOS, sometimes into: `System/Installation/Packages/`)\n" + f" e. Copy '{os.path.basename(apple_diag_src)}' (if found) into: `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/` (or a similar recovery/diagnostics path if known for your version).\n" + f" f. Ensure `boot.efi` (from the OpenCore EFI, often copied from `usr/standalone/i386/boot.efi` inside BaseSystem.dmg or similar) is placed at `System/Library/CoreServices/boot.efi` on the '{installer_vol_label}' partition. (Your EFI setup on partition 1 handles OpenCore booting, this is for the macOS installer itself).\n\n" + + f"3. (Optional but Recommended) Create `.IAProductInfo` file at the root of the '{installer_vol_label}' partition. This file is a symlink to `Install macOS {self.target_macos_version}.app/Contents/SharedSupport/InstallInfo.plist` in real installers. On Windows, you may need to copy the `InstallInfo.plist` to this location as well if symlinks are hard.\n\n" + + "IMPORTANT:\n" + "- Without step 2 (copying additional assets), the USB will likely NOT work as a full offline installer and may only offer Internet Recovery (if OpenCore is correctly configured for network access).\n" + "- The temporary BaseSystem HFS image at '{abs_hfs_path_win}' will be DELETED when you close this program or this message.\n" + ) + self._report_progress(f"GUIDANCE FOR MANUAL STEPS:\n{guidance_message}") + # Use the QMessageBox mock or actual if available + QMessageBox.information(None, f"Manual Steps Required for Windows USB - {self.target_macos_version}", guidance_message) + + self._report_progress("Windows USB installer preparation (EFI automated, macOS content requires manual steps as detailed).") return True except Exception as e: - self._report_progress(f"Error during Windows USB writing: {e}"); self._report_progress(traceback.format_exc()) + self._report_progress(f"FATAL ERROR during Windows USB writing: {e}"); self._report_progress(traceback.format_exc()) + # Show error in QMessageBox as well if possible + QMessageBox.critical(None, "USB Writing Failed", f"An error occurred: {e}\n\n{traceback.format_exc()}") return False finally: if self.assigned_efi_letter: - self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit") + self._report_progress(f"Attempting to remove drive letter assignment for {self.assigned_efi_letter}:") + # Run silently, don't check for errors as it's cleanup + self._run_diskpart_script(f"select volume {self.assigned_efi_letter}\nremove letter={self.assigned_efi_letter}\nexit", capture_output_for_parse=False) + + # Cleanup of self.temp_dir_base will handle all sub-temp-dirs and files within it. self._cleanup_temp_files_and_dirs() + self._report_progress("Temporary files cleanup attempted.") +# Standalone test block if __name__ == '__main__': - import traceback - from constants import MACOS_VERSIONS - if platform.system() != "Windows": print("This script is for Windows standalone testing."); exit(1) + import platform + if platform.system() != "Windows": + print("This script's standalone test mode is intended for Windows.") + # sys.exit(1) # Use sys.exit for proper exit codes + print("USB Writer Windows Standalone Test - Installer Method Guidance") - mock_download_dir = f"temp_macos_download_skyscope_{os.getpid()}"; os.makedirs(mock_download_dir, exist_ok=True) - target_version_cli = sys.argv[1] if len(sys.argv) > 1 else "Sonoma" - mock_product_name_segment = MACOS_VERSIONS.get(target_version_cli, target_version_cli).lower() - mock_product_name = f"000-00000 - macOS {target_version_cli} {mock_product_name_segment}.x.x" - specific_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) - os.makedirs(os.path.join(specific_product_folder, "SharedSupport"), exist_ok=True) - os.makedirs(specific_product_folder, exist_ok=True) - with open(os.path.join(specific_product_folder, "SharedSupport", "BaseSystem.dmg"), "w") as f: f.write("dummy base system dmg") - if not os.path.exists(OC_TEMPLATE_DIR): os.makedirs(OC_TEMPLATE_DIR, exist_ok=True) - if not os.path.exists(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC")): os.makedirs(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC"), exist_ok=True) - with open(os.path.join(OC_TEMPLATE_DIR, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"Test":True}, f, fmt=plistlib.PlistFormat.XML) + # Mock constants if not available (e.g. running totally standalone) + try: from constants import MACOS_VERSIONS + except ImportError: MACOS_VERSIONS = {"Sonoma": "sonoma", "Ventura": "ventura"} ; print("Mocked MACOS_VERSIONS") - disk_id_input = input("Enter target disk NUMBER (e.g., '1' for 'disk 1'). WIPES DISK: ") - if not disk_id_input.isdigit(): print("Invalid disk number."); exit(1) + pid_test = os.getpid() + # Create a unique temp directory for this test run to avoid conflicts + # Place it in user's Temp for better behavior on Windows + test_run_temp_dir = os.path.join(os.environ.get("TEMP", "C:\\Temp"), f"skyscope_test_run_{pid_test}") + os.makedirs(test_run_temp_dir, exist_ok=True) - if input(f"Sure to wipe disk {disk_id_input}? (yes/NO): ").lower() == 'yes': - writer = USBWriterWindows(disk_id_input, mock_download_dir, print, True, target_version_cli) - writer.format_and_write() - else: print("Cancelled.") - shutil.rmtree(mock_download_dir, ignore_errors=True); - # shutil.rmtree(OC_TEMPLATE_DIR, ignore_errors=True) # Keep template for other tests potentially - print("Mock download dir cleaned up.") + # Mock download directory structure within the test_run_temp_dir + mock_download_dir = os.path.join(test_run_temp_dir, "mock_macos_downloads") + os.makedirs(mock_download_dir, exist_ok=True) + # Example: Sonoma. More versions could be added for thorough testing. + target_version_test = "Sonoma" + version_tag_test = MACOS_VERSIONS.get(target_version_test, target_version_test.lower()) + + mock_product_name = f"012-34567 - macOS {target_version_test} 14.1" # Example name + mock_product_folder = os.path.join(mock_download_dir, "macOS Downloads", "publicrelease", mock_product_name) + mock_shared_support = os.path.join(mock_product_folder, "SharedSupport") + os.makedirs(mock_shared_support, exist_ok=True) + + # Create dummy files that would be found by _find_gibmacos_asset and _extract_hfs_from_dmg_or_pkg + # 1. Dummy InstallAssistant.pkg (which contains BaseSystem.dmg) + dummy_pkg_path = os.path.join(mock_product_folder, "InstallAssistant.pkg") + with open(dummy_pkg_path, "wb") as f: f.write(os.urandom(10*1024*1024)) # 10MB dummy PKG + # For the _extract_hfs_from_dmg_or_pkg to work with 7z, it needs a real archive. + # This test won't actually run 7z unless 7z is installed and the dummy files are valid archives. + # The focus here is testing the script logic, not 7z itself. + # So, we'll also create a dummy extracted BaseSystem.hfs for the guidance part. + + # 2. Dummy files for the guidance message (these would normally be in mock_product_folder or mock_shared_support) + with open(os.path.join(mock_product_folder, "InstallInfo.plist"), "w") as f: f.write("") + with open(os.path.join(mock_shared_support, "BaseSystem.dmg"), "wb") as f: f.write(os.urandom(5*1024*1024)) # Dummy DMG + with open(os.path.join(mock_shared_support, "BaseSystem.chunklist"), "w") as f: f.write("chunklist content") + # AppleDiagnostics.dmg is optional + with open(os.path.join(mock_shared_support, "AppleDiagnostics.dmg"), "wb") as f: f.write(os.urandom(1*1024*1024)) + + + # Ensure OC_TEMPLATE_DIR (EFI_template_installer) exists for the test or use the minimal creation. + # Relative path from usb_writer_windows.py to EFI_template_installer + abs_oc_template_dir = OC_TEMPLATE_DIR + if not os.path.exists(abs_oc_template_dir): + print(f"Warning: Test OC_TEMPLATE_DIR '{abs_oc_template_dir}' not found. Minimal EFI will be created by script if needed.") + # Optionally, create a dummy one for test if you want to test the copy logic: + # os.makedirs(os.path.join(abs_oc_template_dir, "EFI", "OC"), exist_ok=True) + # with open(os.path.join(abs_oc_template_dir, "EFI", "OC", "config-template.plist"), "wb") as f: plistlib.dump({"TestTemplate":True}, f) + else: + print(f"Using existing OC_TEMPLATE_DIR for test: {abs_oc_template_dir}") + + + disk_id_input = input("Enter target PHYSICAL DISK NUMBER for test (e.g., '1' for PhysicalDrive1). WARNING: THIS DISK WILL BE MODIFIED/WIPED by diskpart. BE ABSOLUTELY SURE. Enter 'skip' to not run diskpart stage: ") + + if disk_id_input.lower() == 'skip': + print("Skipping disk operations. Guidance message will be shown with placeholder disk info.") + # Create a writer instance with a dummy disk ID for logic testing without diskpart + writer = USBWriterWindows("disk 0", mock_download_dir, print, True, target_version_test) + # We need to manually create a dummy temp_basesystem.hfs for the guidance message part + os.makedirs(writer.temp_dir_base, exist_ok=True) + with open(writer.temp_basesystem_hfs_path, "wb") as f: f.write(os.urandom(1024*1024)) # 1MB dummy HFS + # Manually call parts of format_and_write that don't involve diskpart + writer.check_dependencies() # Still check other deps + # Simulate EFI setup success for guidance + writer.assigned_efi_letter = "X" + # ... then generate and show guidance (this part is inside format_and_write) + # This is a bit clunky for 'skip' mode. Full format_and_write is better if safe. + print("Test in 'skip' mode is limited. Full test requires a dedicated test disk.") + + elif not disk_id_input.isdigit(): + print("Invalid disk number.") + else: + actual_disk_id_str = f"\\\\.\\PhysicalDrive{disk_id_input}" # Match format used by class + confirm = input(f"ARE YOU ABSOLUTELY SURE you want to test on {actual_disk_id_str}? This involves running 'diskpart clean'. Type 'YESIDO' to confirm: ") + if confirm == 'YESIDO': + writer = USBWriterWindows(actual_disk_id_str, mock_download_dir, print, True, target_version_test) + try: + writer.format_and_write() + print(f"Test run completed. Check disk {disk_id_input} and console output.") + except Exception as e: + print(f"Test run failed: {e}") + traceback.print_exc() + else: + print("Test cancelled by user.") + + # Cleanup the test run's unique temp directory + print(f"Cleaning up test run temp directory: {test_run_temp_dir}") + shutil.rmtree(test_run_temp_dir, ignore_errors=True) + + print("Standalone test finished.") ``` -This refactors `usb_writer_windows.py`: -- Updates `__init__` for `macos_download_path`. -- `format_and_write` now: - - Partitions with `diskpart` (EFI + HFS+ type for macOS partition). - - Sets up OpenCore EFI on ESP from `EFI_template_installer` (with `plist_modifier` call). - - Extracts `BaseSystem.hfs` using `7z`. - - Provides detailed guidance for manual `dd` of `BaseSystem.hfs` and manual copying of other installer assets, including partition number and offset. -- `qemu-img` is removed from dependencies. -- Standalone test updated.