diff --git a/OpenCore-Patcher.command b/OpenCore-Patcher.command index fe892f4e5..d5f3758d3 100755 --- a/OpenCore-Patcher.command +++ b/OpenCore-Patcher.command @@ -17,7 +17,7 @@ class OpenCoreLegacyPatcher: self.constants = constants.Constants() self.generate_base_data() if utilities.check_cli_args() is None: - self.main_menu() + self.main_menu(True) def generate_base_data(self): self.constants.detected_os = os_probe.detect_kernel_major() @@ -37,8 +37,34 @@ class OpenCoreLegacyPatcher: arguments.arguments().parse_arguments(self.constants) else: print("- No arguments present, loading TUI") + + def first_setup(self): + # Order of operations: + # 1. Build OpenCore + # 2. Download macOS + # 3. Select USB drive + # 4. Format USB drive + # 5. Install OpenCore to ESP + # 6. flash macOS + # 7. Prompt user to reboot + build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore() + cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).download_macOS() + utilities.cls() - def main_menu(self): + sys.exit(0) + + + def post_install(self): + # Order of operations: + # 1. Build OpenCore + # 2. Prompt drive to install OC to + # 3. Install OpenCore to ESP + # 4. Determine whether root patching needed + # 5. Prompt user to reboot + print() + + + def main_menu(self, walkthrough): response = None while not (response and response == -1): title = [ @@ -67,17 +93,31 @@ class OpenCoreLegacyPatcher: menu = utilities.TUIMenu(title, "Please select an option: ", in_between=in_between, auto_number=True, top_level=True) - options = ( - [["Build OpenCore", build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore]] - if ((self.constants.custom_model or self.computer.real_model) in model_array.SupportedSMBIOS) or self.constants.allow_oc_everywhere is True - else [] - ) + [ - ["Install OpenCore to USB/internal drive", install.tui_disk_installation(self.constants).copy_efi], - ["Post-Install Volume Patch", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).PatchVolume], - ["Change Model", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).change_model], - ["Patcher Settings", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).patcher_settings], - ["Credits", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).credits], - ] + if walkthrough is True: + options = ( + [["First Time Setup", self.first_setup]] + if ((self.constants.custom_model or self.computer.real_model) in model_array.SupportedSMBIOS) or self.constants.allow_oc_everywhere is True + else [] + ) + [ + #["Post-Installation setup", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).closing_message], + ["Change Model", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).change_model], + ["Patcher Settings", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).patcher_settings], + ["Credits", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).credits], + ["Legacy Menu", lambda: self.main_menu(False)], + ] + else: + options = ( + [["Build OpenCore", build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore]] + if ((self.constants.custom_model or self.computer.real_model) in model_array.SupportedSMBIOS) or self.constants.allow_oc_everywhere is True + else [] + ) + [ + ["Install OpenCore to USB/internal drive", install.tui_disk_installation(self.constants).copy_efi], + ["Post-Install Volume Patch", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).PatchVolume], + ["Change Model", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).change_model], + ["Patcher Settings", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).patcher_settings], + ["Installer Creation", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).download_macOS], + ["Credits", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).credits], + ] for option in options: menu.add_menu_option(option[0], function=option[1]) diff --git a/resources/build.py b/resources/build.py index 62d6dc5e8..72e3f37ab 100644 --- a/resources/build.py +++ b/resources/build.py @@ -1074,4 +1074,4 @@ class BuildOpenCore: print(f" {self.constants.opencore_release_folder}") print("") if self.constants.gui_mode is False: - input("Press [Enter] to go back.\n") + input("Press [Enter] to continue\n") diff --git a/resources/cli_menu.py b/resources/cli_menu.py index 2909ed233..2307b9685 100644 --- a/resources/cli_menu.py +++ b/resources/cli_menu.py @@ -1,9 +1,8 @@ # Handle misc CLI menu options # Copyright (C) 2020-2021, Dhinak G, Mykola Grymalyuk from __future__ import print_function -import subprocess -from resources import constants, utilities, defaults, sys_patch +from resources import constants, install, utilities, defaults, sys_patch, installer from data import cpu_data, smbios_data, model_array, os_data @@ -1038,6 +1037,96 @@ system_profiler SPHardwareDataType | grep 'Model Identifier' menu.add_menu_option(option[0], function=option[1]) response = menu.start() + + def download_macOS(self): + utilities.cls() + utilities.header(["Create macOS installer"]) + print( + """ +This option allows you to download and flash a macOS installer +to your USB drive. + +1. Download macOS Installer +2. Use Existing Installer +""" + ) + change_menu = input("Select an option: ") + if change_menu == "1": + self.download_macOS_installer() + elif change_menu == "2": + self.find_local_installer() + else: + self.download_macOS() + + def download_install_assistant(self, link): + installer.download_install_assistant(self.constants.payload_path, link) + # To avoid selecting the wrong installer by mistake, let user select the correct one + self.find_local_installer() + + + def download_macOS_installer(self): + response = None + while not (response and response == -1): + options = [] + title = ["Select the macOS Installer you wish to download"] + menu = utilities.TUIMenu(title, "Please select an option: ", auto_number=True, top_level=True) + avalible_installers = installer.list_downloadable_macOS_installers(self.constants.payload_path, "DeveloperSeed") + if avalible_installers: + for app in avalible_installers: + options.append([f"macOS {avalible_installers[app]['Version']} ({avalible_installers[app]['Build']} - {utilities.human_fmt(avalible_installers[app]['Size'])})", lambda x=app: self.download_install_assistant(avalible_installers[x]['Link'])]) + for option in options: + menu.add_menu_option(option[0], function=option[1]) + response = menu.start() + + def find_local_installer(self): + response = None + while not (response and response == -1): + options = [] + title = ["Select the macOS Installer you wish to use"] + menu = utilities.TUIMenu(title, "Please select an option: ", auto_number=True, top_level=True) + avalible_installers = installer.list_local_macOS_installers() + if avalible_installers: + for app in avalible_installers: + options.append([f"{avalible_installers[app]['Short Name']}: {avalible_installers[app]['Version']} ({avalible_installers[app]['Build']})", lambda: self.list_disks(avalible_installers[app]['Path'])]) + for option in options: + menu.add_menu_option(option[0], function=option[1]) + response = menu.start() + + def list_disks(self, installer_path): + disk = installer.select_disk_to_format() + if disk != None: + if installer.format_drive(disk) is True: + # Only install if OC is found + # Allows a user to create a macOS Installer without OCLP if desired + if self.constants.opencore_release_folder.exists(): + # ESP will always be the first partition when formatted by disk utility + install.tui_disk_installation.install_opencore(self, f"disk{disk}", "1") + if installer.create_installer(installer_path, "OCLP-Installer") is True: + utilities.cls() + utilities.header(["Create macOS installer"]) + print("Installer created successfully.") + input("Press enter to exit.") + self.closing_message() + else: + utilities.cls() + utilities.header(["Create macOS installer"]) + print("Installer creation failed.") + input("Press enter to return to the previous.") + return + else: + exit() + + def closing_message(self): + utilities.cls() + utilities.header(["Create macOS installer"]) + print("Thank you for using OpenCore Legacy Patcher!") + print("Reboot your machine and select EFI Boot to load OpenCore") + print("") + print("If you have any issues, remember to check the guide as well as\nour Discord server:") + print("\n\tGuide: https://dortania.github.io/OpenCore-Legacy-Patcher/") + print("\tDiscord: https://discord.gg/rqdPgH8xSN") + input("\nPress enter to exit: ") + exit() big_sur = """Patches Root volume to fix misc issues such as: diff --git a/resources/install.py b/resources/install.py index 832b9e9d4..1f7c0a4c5 100644 --- a/resources/install.py +++ b/resources/install.py @@ -12,20 +12,6 @@ from data import os_data class tui_disk_installation: def __init__(self, versions): self.constants: constants.Constants = versions - - def determine_sd_card(self, media_name): - # Array filled with common SD Card names - # Note most USB-based SD Card readers generally report as "Storage Device" - # Thus no reliable way to detect further without parsing IOService output (kUSBProductString) - if ( - "SD Card" in media_name or \ - "SD/MMC" in media_name or \ - "SDXC Reader" in media_name or \ - "SD Reader" in media_name or \ - "Card Reader" in media_name - ): - return True - return False def copy_efi(self): utilities.cls() @@ -108,8 +94,24 @@ Please build OpenCore first!""" response = menu.start() if response == -1: - return + return + self.install_opencore(disk_identifier, response) + def install_opencore(self, disk_identifier, response): + def determine_sd_card(media_name): + # Array filled with common SD Card names + # Note most USB-based SD Card readers generally report as "Storage Device" + # Thus no reliable way to detect further without parsing IOService output (kUSBProductString) + if ( + "SD Card" in media_name or \ + "SD/MMC" in media_name or \ + "SDXC Reader" in media_name or \ + "SD Reader" in media_name or \ + "Card Reader" in media_name + ): + return True + return False + # TODO: Apple Script fails in Yosemite(?) and older args = [ "osascript", @@ -134,8 +136,6 @@ Please build OpenCore first!""" ["Copying OpenCore"], "Press [Enter] to go back.\n", ["An error occurred!"] + result.stderr.decode().split("\n") + ["", "Please report this to the devs at GitHub."] ).start() return - - # TODO: Remount if readonly drive_host_info = plistlib.loads(subprocess.run(f"diskutil info -plist {disk_identifier}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode()) partition_info = plistlib.loads(subprocess.run(f"diskutil info -plist {disk_identifier}s{response}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode()) sd_type = drive_host_info["MediaName"] @@ -178,7 +178,7 @@ Please build OpenCore first!""" Path(mount_path / Path("EFI/BOOT")).mkdir() shutil.move(mount_path / Path("System/Library/CoreServices/boot.efi"), mount_path / Path("EFI/BOOT/BOOTx64.efi")) shutil.rmtree(mount_path / Path("System"), onerror=rmtree_handler) - if self.determine_sd_card(sd_type) is True: + if determine_sd_card(sd_type) is True: print("- Adding SD Card icon") shutil.copy(self.constants.icon_path_sd, mount_path) elif ssd_type is True: diff --git a/resources/installer.py b/resources/installer.py new file mode 100644 index 000000000..8aceac19b --- /dev/null +++ b/resources/installer.py @@ -0,0 +1,174 @@ +# Creates a macOS Installer +from pathlib import Path +import plistlib +import subprocess +from resources import utilities + +def list_local_macOS_installers(): + # Finds all applicable macOS installers + # within a user's /Applications folder + # Returns a list of installers + application_list = {} + + for application in Path("/Applications").iterdir(): + # Verify whether application has createinstallmedia + if (Path("/Applications") / Path(application) / Path("Contents/Resources/createinstallmedia")).exists(): + plist = plistlib.load((Path("/Applications") / Path(application) / Path("Contents/Info.plist")).open("rb")) + try: + # Doesn't reflect true OS build, but best to report SDK in the event multiple installers are found with same version + app_version = plist["DTPlatformVersion"] + clean_name = plist["CFBundleDisplayName"] + try: + app_sdk = plist["DTSDKBuild"] + except KeyError: + app_sdk = "Unknown" + application_list.update({ + application: { + "Short Name": clean_name, + "Version": app_version, + "Build": app_sdk, + "Path": application, + } + }) + except KeyError: + pass + return application_list + +def create_installer(installer_path, volume_name): + # Creates a macOS installer + # Takes a path to the installer and the Volume + # Returns boolean on success status + + createinstallmedia_path = Path("/Applications") / Path(installer_path) / Path("Contents/Resources/createinstallmedia") + + # Sanity check in the event the user somehow deleted it between the time we found it and now + if (createinstallmedia_path).exists(): + utilities.cls() + utilities.header(["Starting createinstallmedia"]) + print("This will take some time, recommend making some coffee while you wait\n") + utilities.elevated([createinstallmedia_path, "--volume", f"/Volumes/{volume_name}", "--nointeraction"]) + return True + else: + print("- Failed to find createinstallmedia") + return False + +def download_install_assistant(download_path, ia_link): + # Downloads and unpackages InstallAssistant.pkg into /Applications + utilities.download_file(ia_link, (Path(download_path) / Path("InstallAssistant.pkg"))) + print("- Installing InstallAssistant.pkg to /Applications/") + utilities.elevated(["installer", "-pkg", (Path(download_path) / Path("InstallAssistant.pkg")), "-target", "/Applications"]) + input("- Press ENTER to continue: ") + +def list_downloadable_macOS_installers(download_path, catalog): + avalible_apps = {} + if catalog == "DeveloperSeed": + link = "https://swscan.apple.com/content/catalogs/others/index-12seed-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz" + elif catalog == "PublicSeed": + link = "https://swscan.apple.com/content/catalogs/others/index-12beta-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz" + else: + link = "https://swscan.apple.com/content/catalogs/others/index-12customerseed-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz" + + # Download and unzip the catalog + utilities.download_file(link, (Path(download_path) / Path("seed.sucatalog.gz"))) + subprocess.run(["gunzip", "-d", "-f", Path(download_path) / Path("seed.sucatalog.gz")]) + catalog_plist = plistlib.load((Path(download_path) / Path("seed.sucatalog")).open("rb")) + + for item in catalog_plist["Products"]: + try: + # Check if entry has SharedSupport and BuildManifest + # Ensures only Big Sur and newer Installers are listed + catalog_plist["Products"][item]["ExtendedMetaInfo"]["InstallAssistantPackageIdentifiers"]["SharedSupport"] + catalog_plist["Products"][item]["ExtendedMetaInfo"]["InstallAssistantPackageIdentifiers"]["BuildManifest"] + + for bm_package in catalog_plist["Products"][item]["Packages"]: + if "BuildManifest.plist" in bm_package["URL"]: + utilities.download_file(bm_package["URL"], (Path(download_path) / Path("BuildManifest.plist"))) + build_plist = plistlib.load((Path(download_path) / Path("BuildManifest.plist")).open("rb")) + version = build_plist["ProductVersion"] + build = build_plist["ProductBuildVersion"] + for ia_package in catalog_plist["Products"][item]["Packages"]: + if "InstallAssistant.pkg" in ia_package["URL"]: + download_link = ia_package["URL"] + size = ia_package["Size"] + integirty = ia_package["IntegrityDataURL"] + + avalible_apps.update({ + item: { + "Version": version, + "Build": build, + "Link": download_link, + "Size": size, + "integirty": integirty, + } + }) + except KeyError: + pass + return avalible_apps + +def format_drive(disk_id): + # Formats a disk for macOS install + # Takes a disk ID + # Returns boolean on success status + header = f"# Formatting disk{disk_id} for macOS installer #" + box_length = len(header) + utilities.cls() + print("#" * box_length) + print(header) + print("#" * box_length) + print("") + #print(f"- Formatting disk{disk_id} for macOS installer") + format_process = utilities.elevated(["diskutil", "eraseDisk", "HFS+", "OCLP-Installer", f"disk{disk_id}"]) + if format_process.returncode == 0: + print("- Disk formatted") + return True + else: + print("- Failed to format disk") + print(f" Error Code: {format_process.returncode}") + input("\nPress Enter to exit") + return False + +def select_disk_to_format(): + utilities.cls() + utilities.header(["Installing OpenCore to Drive"]) + + print("\nDisk picker is loading...") + + all_disks = {} + # TODO: AllDisksAndPartitions is not supported in Snow Leopard and older + try: + # High Sierra and newer + disks = plistlib.loads(subprocess.run("diskutil list -plist physical".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode()) + except ValueError: + # Sierra and older + disks = plistlib.loads(subprocess.run("diskutil list -plist".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode()) + for disk in disks["AllDisksAndPartitions"]: + disk_info = plistlib.loads(subprocess.run(f"diskutil info -plist {disk['DeviceIdentifier']}".split(), stdout=subprocess.PIPE).stdout.decode().strip().encode()) + try: + all_disks[disk["DeviceIdentifier"]] = {"identifier": disk_info["DeviceNode"], "name": disk_info["MediaName"], "size": disk_info["TotalSize"], "removable": disk_info["Internal"], "partitions": {}} + except KeyError: + # Avoid crashing with CDs installed + continue + menu = utilities.TUIMenu( + ["Select Disk to write the macOS Installer onto"], + "Please select the disk you would like to install OpenCore to: ", + in_between=["Missing drives? Verify they are 14GB+ and external (ie. USB)", "", "Ensure all data is backed up on selected drive, entire drive will be erased!"], + return_number_instead_of_direct_call=True, + loop=True, + ) + for disk in all_disks: + # Strip disks that are under 14GB (15,032,385,536 bytes) + # createinstallmedia isn't great at detecting if a disk has enough space + if not any(all_disks[disk]['size'] > 15032385536 for partition in all_disks[disk]): + continue + # Strip internal disks as well (avoid user formatting their SSD/HDD) + # Ensure user doesn't format their boot drive + if not any(all_disks[disk]['removable'] is False for partition in all_disks[disk]): + continue + menu.add_menu_option(f"{disk}: {all_disks[disk]['name']} ({utilities.human_fmt(all_disks[disk]['size'])})", key=disk[4:]) + + response = menu.start() + + if response == -1: + return None + + return response \ No newline at end of file