Merge pull request #696 from dortania/walkthrough

Implement macOS InstallAssistant downloader
This commit is contained in:
Mykola Grymalyuk
2021-11-05 23:29:08 -06:00
committed by GitHub
8 changed files with 314 additions and 26 deletions

View File

@@ -76,6 +76,7 @@ class OpenCoreLegacyPatcher:
["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],
]

11
data/mirror_data.py Normal file
View File

@@ -0,0 +1,11 @@
# Mirrors of Apple's InstallAssistant.ppkg
# Currently only listing important Installers no longer on Apple's servers
Install_macOS_Big_Sur_11_2_3 = {
"Version": "11.2.3",
"Build": "20D91",
"Link": "https://archive.org/download/install-assistant-20D91/InstallAssistant.pkg",
"Size": 12211077798,
"Source": "Archive.org",
"integrity": None,
}

View File

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

View File

@@ -1,10 +1,10 @@
# Handle misc CLI menu options
# Copyright (C) 2020-2021, Dhinak G, Mykola Grymalyuk
from __future__ import print_function
import subprocess
import sys
from resources import constants, utilities, defaults, sys_patch
from data import cpu_data, smbios_data, model_array, os_data
from resources import constants, install, utilities, defaults, sys_patch, installer
from data import cpu_data, smbios_data, model_array, os_data, mirror_data
class MenuOptions:
@@ -1038,6 +1038,100 @@ 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:
# Add mirror of 11.2.3 for users who want it
options.append([f"macOS {mirror_data.Install_macOS_Big_Sur_11_2_3['Version']} ({mirror_data.Install_macOS_Big_Sur_11_2_3['Build']} - {utilities.human_fmt(mirror_data.Install_macOS_Big_Sur_11_2_3['Size'])} - {mirror_data.Install_macOS_Big_Sur_11_2_3['Source']})", lambda: self.download_install_assistant(mirror_data.Install_macOS_Big_Sur_11_2_3['Link'])])
for app in avalible_installers:
options.append([f"macOS {avalible_installers[app]['Version']} ({avalible_installers[app]['Build']} - {utilities.human_fmt(avalible_installers[app]['Size'])} - {avalible_installers[app]['Source']})", 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() and self.constants.walkthrough is True:
# 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.")
if self.constants.walkthrough is True:
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:
if self.constants.walkthrough is True:
sys.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: ")
sys.exit()
big_sur = """Patches Root volume to fix misc issues such as:

View File

@@ -160,7 +160,8 @@ class Constants:
self.disable_msr_power_ctl = False # Disable MSR Power Control (missing battery throttling)
self.software_demux = False # Enable Software Demux patch set
self.force_vmm = False # Force VMM patch
self.custom_sip_value = None # Set custom SIP value
self.custom_sip_value = None # Set custom SIP value
self.walkthrough = False # Enable Walkthrough
self.legacy_accel_support = [
os_data.os_data.mojave,

View File

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

175
resources/installer.py Normal file
View File

@@ -0,0 +1,175 @@
# 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"]
integrity = ia_package["IntegrityDataURL"]
avalible_apps.update({
item: {
"Version": version,
"Build": build,
"Link": download_link,
"Size": size,
"integrity": integrity,
"Source": "Apple Inc.",
}
})
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

View File

@@ -293,6 +293,11 @@ def get_rom(variable: str, *, decode: bool = False):
def download_file(link, location):
if Path(location).exists():
Path(location).unlink()
try:
# Handle cases where Content-Length has garbage or is missing
size_string = f" of {int(requests.head(link).headers['Content-Length']) / 1024 / 1024}MB"
except KeyError:
size_string = ""
response = requests.get(link, stream=True)
short_link = os.path.basename(link)
# SU Catalog's link is quite long, strip to make it bearable
@@ -300,17 +305,18 @@ def download_file(link, location):
short_link = "sucatalog.gz"
header = f"# Downloading: {short_link} #"
box_length = len(header)
box_string = "#" * box_length
with location.open("wb") as file:
count = 0
for chunk in response.iter_content(1024 * 1024 * 4):
file.write(chunk)
count += len(chunk)
cls()
print("#" * box_length)
print(box_string)
print(header)
print("#" * box_length)
print(box_string)
print("")
print(f"{count / 1024 / 1024}MB Downloaded")
print(f"{count / 1024 / 1024}MB Downloaded{size_string}")
checksum = hashlib.sha256()
with location.open("rb") as file:
chunk = file.read(1024 * 1024 * 16)