mirror of
https://github.com/dortania/OpenCore-Legacy-Patcher.git
synced 2026-04-14 12:48:18 +10:00
Merge pull request #696 from dortania/walkthrough
Implement macOS InstallAssistant downloader
This commit is contained in:
@@ -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
11
data/mirror_data.py
Normal 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,
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
175
resources/installer.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user