macOS Installer Creation: initial commit

This commit is contained in:
Mykola Grymalyuk
2021-11-02 18:32:10 -06:00
parent 8fc1bf9572
commit 4929715830
5 changed files with 337 additions and 34 deletions

View File

@@ -17,7 +17,7 @@ class OpenCoreLegacyPatcher:
self.constants = constants.Constants() self.constants = constants.Constants()
self.generate_base_data() self.generate_base_data()
if utilities.check_cli_args() is None: if utilities.check_cli_args() is None:
self.main_menu() self.main_menu(True)
def generate_base_data(self): def generate_base_data(self):
self.constants.detected_os = os_probe.detect_kernel_major() self.constants.detected_os = os_probe.detect_kernel_major()
@@ -38,7 +38,33 @@ class OpenCoreLegacyPatcher:
else: else:
print("- No arguments present, loading TUI") print("- No arguments present, loading TUI")
def main_menu(self): 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()
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 response = None
while not (response and response == -1): while not (response and response == -1):
title = [ title = [
@@ -67,6 +93,19 @@ class OpenCoreLegacyPatcher:
menu = utilities.TUIMenu(title, "Please select an option: ", in_between=in_between, auto_number=True, top_level=True) menu = utilities.TUIMenu(title, "Please select an option: ", in_between=in_between, auto_number=True, top_level=True)
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 = ( options = (
[["Build OpenCore", build.BuildOpenCore(self.constants.custom_model or self.constants.computer.real_model, self.constants).build_opencore]] [["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 if ((self.constants.custom_model or self.computer.real_model) in model_array.SupportedSMBIOS) or self.constants.allow_oc_everywhere is True
@@ -76,6 +115,7 @@ class OpenCoreLegacyPatcher:
["Post-Install Volume Patch", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).PatchVolume], ["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], ["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], ["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], ["Credits", cli_menu.MenuOptions(self.constants.custom_model or self.computer.real_model, self.constants).credits],
] ]

View File

@@ -1074,4 +1074,4 @@ class BuildOpenCore:
print(f" {self.constants.opencore_release_folder}") print(f" {self.constants.opencore_release_folder}")
print("") print("")
if self.constants.gui_mode is False: if self.constants.gui_mode is False:
input("Press [Enter] to go back.\n") input("Press [Enter] to continue\n")

View File

@@ -1,9 +1,8 @@
# Handle misc CLI menu options # Handle misc CLI menu options
# Copyright (C) 2020-2021, Dhinak G, Mykola Grymalyuk # Copyright (C) 2020-2021, Dhinak G, Mykola Grymalyuk
from __future__ import print_function 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 from data import cpu_data, smbios_data, model_array, os_data
@@ -1039,6 +1038,96 @@ system_profiler SPHardwareDataType | grep 'Model Identifier'
response = menu.start() 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: big_sur = """Patches Root volume to fix misc issues such as:
- Non-Metal Graphics Acceleration - Non-Metal Graphics Acceleration

View File

@@ -13,20 +13,6 @@ class tui_disk_installation:
def __init__(self, versions): def __init__(self, versions):
self.constants: constants.Constants = 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): def copy_efi(self):
utilities.cls() utilities.cls()
utilities.header(["Installing OpenCore to Drive"]) utilities.header(["Installing OpenCore to Drive"])
@@ -109,6 +95,22 @@ Please build OpenCore first!"""
if response == -1: 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 # TODO: Apple Script fails in Yosemite(?) and older
args = [ args = [
@@ -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."] ["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() ).start()
return 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()) 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()) 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"] sd_type = drive_host_info["MediaName"]
@@ -178,7 +178,7 @@ Please build OpenCore first!"""
Path(mount_path / Path("EFI/BOOT")).mkdir() 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.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) 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") print("- Adding SD Card icon")
shutil.copy(self.constants.icon_path_sd, mount_path) shutil.copy(self.constants.icon_path_sd, mount_path)
elif ssd_type is True: elif ssd_type is True:

174
resources/installer.py Normal file
View File

@@ -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