def make_plist(self, oc_acpi, cl_acpi, patches): # if not len(patches): return # No patches to add - bail repeat = False print("Building patches_OC and patches_Clover plists...") output = self.d.check_output(self.output) oc_plist = {} cl_plist = {} # Check for the plists if os.path.isfile(os.path.join(output,"patches_OC.plist")): e = os.path.join(output,"patches_OC.plist") with open(e, "rb") as f: oc_plist = plist.load(f) if os.path.isfile(os.path.join(output,"patches_Clover.plist")): e = os.path.join(output,"patches_Clover.plist") with open(e,"rb") as f: cl_plist = plist.load(f) # Ensure all the pathing is where it needs to be oc_plist = self.ensure_path(oc_plist,("ACPI","Add")) oc_plist = self.ensure_path(oc_plist,("ACPI","Patch")) cl_plist = self.ensure_path(cl_plist,("ACPI","SortedOrder")) cl_plist = self.ensure_path(cl_plist,("ACPI","DSDT","Patches")) # Add the .aml references if any(oc_acpi["Comment"] == x["Comment"] for x in oc_plist["ACPI"]["Add"]): print(" -> Add \"{}\" already in OC plist!".format(oc_acpi["Comment"])) else: oc_plist["ACPI"]["Add"].append(oc_acpi) if any(cl_acpi == x for x in cl_plist["ACPI"]["SortedOrder"]): print(" -> \"{}\" already in Clover plist!".format(cl_acpi)) else: cl_plist["ACPI"]["SortedOrder"].append(cl_acpi) # Iterate the patches for p in patches: if any(x["Comment"] == p["Comment"] for x in oc_plist["ACPI"]["Patch"]): print(" -> Patch \"{}\" already in OC plist!".format(p["Comment"])) else: print(" -> Adding Patch \"{}\" to OC plist!".format(p["Comment"])) oc_plist["ACPI"]["Patch"].append(self.get_oc_patch(p)) if any(x["Comment"] == p["Comment"] for x in cl_plist["ACPI"]["DSDT"]["Patches"]): print(" -> Patch \"{}\" already in Clover plist!".format(p["Comment"])) else: print(" -> Adding Patch \"{}\" to Clover plist!".format(p["Comment"])) cl_plist["ACPI"]["DSDT"]["Patches"].append(self.get_clover_patch(p)) # Write the plists with open(os.path.join(output,"patches_OC.plist"),"wb") as f: plist.dump(oc_plist,f) with open(os.path.join(output,"patches_Clover.plist"),"wb") as f: plist.dump(cl_plist,f)
def select_plist(self): while True: self.u.head("Select Plist") print("") print("M. Return To Menu") print("Q. Quit") print("") plist_path = self.u.grab( "Please drag and drop your config.plist here: ") if not len(plist_path): continue elif plist_path.lower() == "m": return elif plist_path.lower() == "q": self.u.custom_quit() path_checked = self.u.check_path(plist_path) if not path_checked: continue # Got a valid path here - let's try to load it try: with open(path_checked, "rb") as f: plist_data = plist.load(f) if not isinstance(plist_data, dict): raise Exception("Plist root is not a dictionary") except Exception as e: self.u.head("Error Loading Plist") print("\nCould not load {}:\n\n{}\n\n".format( path_checked, repr(e))) self.u.grab("Press [enter] to return...") continue # Got valid plist data - let's store the vars and return self.plist_path = path_checked self.plist_data = plist_data return (path_checked, plist_data)
def __init__(self): self.u = utils.Utils("OC Snapshot") self.snapshot_data = {} self.safe_path_length = 128 # OC_STORAGE_SAFE_PATH_MAX from Include/Acidanthera/Library/OcStorageLib.h in OpenCorePkg if os.path.exists("Scripts/snapshot.plist"): try: with open("Scripts/snapshot.plist", "rb") as f: self.snapshot_data = plist.load(f) except: pass
def get_catalog_data(self, local=False): # Gets the data based on our current_catalog url = self.build_url(catalog=self.current_catalog, version=self.current_macos) self.u.head("Downloading Catalog") print("") if local: print("Checking locally for {}".format(self.plist)) cwd = os.getcwd() os.chdir(os.path.dirname(os.path.realpath(__file__))) if os.path.exists( os.path.join(os.path.dirname(os.path.realpath(__file__)), self.scripts, self.plist)): print(" - Found - loading...") try: with open( os.path.join(os.getcwd(), self.scripts, self.plist), "rb") as f: self.catalog_data = plist.load(f) os.chdir(cwd) return True except: print(" - Error loading - downloading instead...\n") os.chdir(cwd) else: print(" - Not found - downloading instead...\n") print("Currently downloading {} catalog from\n\n{}\n".format( self.current_catalog, url)) try: b = self.d.get_bytes(url) print("") self.catalog_data = plist.loads(b) except: print("Error downloading!") return False try: # Assume it's valid data - dump it to a local file if local or self.force_local: print(" - Saving to {}...".format(self.plist)) cwd = os.getcwd() os.chdir(os.path.dirname(os.path.realpath(__file__))) with open(os.path.join(os.getcwd(), self.scripts, self.plist), "wb") as f: plist.dump(self.catalog_data, f) os.chdir(cwd) except: print(" - Error saving!") return False return True
def main(self): self.u.resize(self.w, self.h) self.u.head() print("") print("Q. Quit") print("") print("Please drag and drop a USBMap(Legacy).kext, Info.plist,") menu = self.u.grab("or UsbDumpEfi.efi output here to continue: ") if not len(menu): return if menu.lower() == "q": self.u.custom_quit() # Check the path path = self.u.check_path(menu) try: # Ensure we have a valid path if not path: raise Exception("{} does not exist!".format(menu)) if os.path.isdir(path): path = os.path.join(path, "Contents", "Info.plist") if not os.path.exists(path): raise Exception("{} does not exist!".format(path)) if not os.path.isfile(path): raise Exception("{} is a directory!".format(path)) except Exception as e: return self.show_error("Error Selecting Target", e) try: # Load it and ensure the plist is valid with open(path, "rb") as f: raw = f.read().replace(b"\x00", b"").decode("utf-8", errors="ignore") if "UsbDumpEfi start" in raw: return self.parse_usb_txt(raw) else: f.seek(0) plist_data = plist.load(f, dict_type=OrderedDict) except Exception as e: return self.show_error( "Error Loading {}".format(os.path.basename(path)), e) if not len(plist_data.get("IOKitPersonalities", {})): return self.show_error( "Missing Personalities", "No IOKitPersonalities found in {}!".format( os.path.basename(path))) self.plist_path = path self.plist_data = plist_data self.pick_personality()
def _get_plist(self): self.u.head("Select Plist") print("") print("Current: {}".format(self.plist)) print("") print("C. Clear Selection") print("M. Main Menu") print("Q. Quit") print("") p = self.u.grab("Please drag and drop the target plist: ") if p.lower() == "q": self.u.custom_quit() elif p.lower() == "m": return elif p.lower() == "c": self.plist = None self.plist_data = None return pc = self.u.check_path(p) if not pc: self.u.head("File Missing") print("") print("Plist file not found:\n\n{}".format(p)) print("") self.u.grab("Press [enter] to return...") self._get_plist() try: with open(pc, "rb") as f: self.plist = p self.plist_data = plist.load(f) except Exception as e: self.u.head("Plist Malformed") print("") print("Plist file malformed:\n\n{}".format(e)) print("") self.u.grab("Press [enter] to return...") self._get_plist()
def open_plist_with_path(self, event=None, path=None, current_window=None, plist_type="XML"): if path == None: # Uh... wut? return path = os.path.realpath(os.path.expanduser(path)) # Let's try to load the plist try: with open(path, "rb") as f: plist_type = "Binary" if plist._is_binary(f) else "XML" plist_data = plist.load( f, dict_type=dict if self.settings.get("sort_dict", False) else OrderedDict) except Exception as e: # Had an issue, throw up a display box self.tk.bell() mb.showerror("An Error Occurred While Opening {}".format( os.path.basename(path)), str(e)) # ,parent=current_window) return None # Opened it correctly - let's load it, and set our values if current_window: current_window.open_plist( path, plist_data, plist_type, self.settings.get("expand_all_items_on_open", True)) else: # Need to create one first current_window = plistwindow.PlistWindow(self, self.tk) current_window.open_plist( path, plist_data, plist_type, self.settings.get("expand_all_items_on_open", True)) current_window.focus_force() current_window.update() return True
def snapshot(self, in_file=None, out_file=None, oc_folder=None, clean=False): oc_folder = self.u.check_path(oc_folder) if not oc_folder: print("OC folder passed does not exist!") exit(1) if not os.path.isdir(oc_folder): print("OC folder passed is not a directory!") exit(1) if in_file: in_file = self.u.check_path(in_file) if not in_file: print("Input plist passed does not exist!") exit(1) if os.path.isdir(in_file): print("Input plist passed is a directory!") exit(1) try: with open(in_file, "rb") as f: tree_dict = plist.load(f) except Exception as e: print("Error loading plist: {}".format(e)) exit(1) else: if not out_file: print("At least one input or output file must be provided.") exit(1) # We got an out file at least - create an empty dict for the in_file tree_dict = {} if not out_file: out_file = in_file # Verify folder structure - should be as follows: # OC # +- ACPI # | +- SSDT.aml # +- Drivers # | +- EfiDriver.efi # +- Kexts # | +- Something.kext # +- config.plist # +- Tools (Optional) # | +- SomeTool.efi # | +- SomeFolder # | | +- SomeOtherTool.efi oc_acpi = os.path.normpath(os.path.join(oc_folder, "ACPI")) oc_drivers = os.path.normpath(os.path.join(oc_folder, "Drivers")) oc_kexts = os.path.normpath(os.path.join(oc_folder, "Kexts")) oc_tools = os.path.normpath(os.path.join(oc_folder, "Tools")) oc_efi = os.path.normpath(os.path.join(oc_folder, "OpenCore.efi")) for x in (oc_acpi, oc_drivers, oc_kexts): if not os.path.exists(x): print("Incorrect OC Folder Struction - {} does not exist.". format(x)) exit(1) if x != oc_efi and not os.path.isdir(x): print( "Incorrect OC Folder Struction - {} exists, but is not a directory." .format(x)) exit(1) # Folders are valid - lets work through each section # Let's get the hash of OpenCore.efi, compare to a known list, and then compare that version to our snapshot_version if found hasher = hashlib.md5() try: with open(oc_efi, "rb") as f: hasher.update(f.read()) oc_hash = hasher.hexdigest() except: oc_hash = "" # Couldn't determine hash :( # Let's get the version of the snapshot that matches our target, and that matches our hash if any latest_snap = {} # Highest min_version target_snap = {} # Matches our hash for snap in self.snapshot_data: hashes = snap.get("release_hashes", []) hashes.extend(snap.get("debug_hashes", [])) # Retain the highest version we see if snap.get("min_version", "0.0.0") > latest_snap.get( "min_version", "0.0.0"): latest_snap = snap # Also retain the last snap that matches our hash if len(oc_hash) and (oc_hash in snap.get("release_hashes", []) or oc_hash in snap.get("debug_hashes", [])): target_snap = snap if not target_snap: target_snap = latest_snap # Apply our snapshot values acpi_add = target_snap.get("acpi_add", {}) kext_add = target_snap.get("kext_add", {}) tool_add = target_snap.get("tool_add", {}) driver_add = target_snap.get("driver_add", {}) long_paths = [ ] # We'll add any paths that exceed the OC_STORAGE_SAFE_PATH_MAX of 128 chars # ACPI is first, we'll iterate the .aml files we have and add what is missing # while also removing what exists in the plist and not in the folder. # If something exists in the table already, we won't touch it. This leaves the # enabled and comment properties untouched. # # Let's make sure we have the ACPI -> Add sections in our config # We're going to replace the whole list if not "ACPI" in tree_dict or not isinstance(tree_dict["ACPI"], dict): tree_dict["ACPI"] = {"Add": []} if not "Add" in tree_dict["ACPI"] or not isinstance( tree_dict["ACPI"]["Add"], list): tree_dict["ACPI"]["Add"] = [] # Now we walk the existing add values new_acpi = [] for path, subdirs, files in os.walk(oc_acpi): for name in files: if not name.startswith(".") and name.lower().endswith(".aml"): new_acpi.append( os.path.join(path, name)[len(oc_acpi):].replace( "\\", "/").lstrip("/")) add = [] if clean else tree_dict["ACPI"]["Add"] for aml in sorted(new_acpi, key=lambda x: x.lower()): if aml.lower() in [ x.get("Path", "").lower() for x in add if isinstance(x, dict) ]: # Found it - skip continue # Doesn't exist, add it new_aml_entry = { "Comment": os.path.basename(aml), "Enabled": True, "Path": aml } # Add our snapshot custom entries, if any for x in acpi_add: new_aml_entry[x] = acpi_add[x] add.append(new_aml_entry) new_add = [] for aml in add: if not isinstance(aml, dict): # Not the right type - skip it continue if not aml.get("Path", "").lower() in [x.lower() for x in new_acpi]: # Not there, skip continue new_add.append(aml) # Check path length long_paths.extend(self.check_path_length(aml)) tree_dict["ACPI"]["Add"] = new_add # Now we need to walk the kexts if not "Kernel" in tree_dict or not isinstance(tree_dict["Kernel"], dict): tree_dict["Kernel"] = {"Add": []} if not "Add" in tree_dict["Kernel"] or not isinstance( tree_dict["Kernel"]["Add"], list): tree_dict["Kernel"]["Add"] = [] kext_list = [] # We need to gather a list of all the files inside that and with .efi for path, subdirs, files in os.walk(oc_kexts): for name in sorted(subdirs, key=lambda x: x.lower()): if name.startswith(".") or not name.lower().endswith(".kext"): continue kdict = { # "Arch":"Any", "BundlePath": os.path.join(path, name)[len(oc_kexts):].replace( "\\", "/").lstrip("/"), "Comment": name, "Enabled": True, # "MaxKernel":"", # "MinKernel":"", "ExecutablePath": "" } # Add our entries from kext_add as needed for y in kext_add: kdict[y] = kext_add[y] # Get the Info.plist plist_full_path = plist_rel_path = None for kpath, ksubdirs, kfiles in os.walk(os.path.join( path, name)): for kname in kfiles: if kname.lower() == "info.plist": plist_full_path = os.path.join(kpath, kname) plist_rel_path = plist_full_path[ len(os.path.join(path, name)):].replace( "\\", "/").lstrip("/") break if plist_full_path: break # Found it - break else: # Didn't find it - skip continue kdict["PlistPath"] = plist_rel_path # Let's load the plist and check for other info try: with open(plist_full_path, "rb") as f: info_plist = plist.load(f) kinfo = { "CFBundleIdentifier": info_plist.get("CFBundleIdentifier", None), "OSBundleLibraries": info_plist.get("OSBundleLibraries", []) } if info_plist.get("CFBundleExecutable", None): if not os.path.exists( os.path.join( path, name, "Contents", "MacOS", info_plist["CFBundleExecutable"])): continue # Requires an executable that doesn't exist - bail kdict[ "ExecutablePath"] = "Contents/MacOS/" + info_plist[ "CFBundleExecutable"] except Exception as e: continue # Something else broke here - bail # Should have something valid here kext_list.append((kdict, kinfo)) bundle_list = [x[0].get("BundlePath", "") for x in kext_list] kexts = [] if clean else tree_dict["Kernel"]["Add"] original_kexts = [ x for x in kexts if x.get("BundlePath", "") in bundle_list ] # get the original load order for comparison purposes - but omit any that no longer exist for kext, info in kext_list: if kext["BundlePath"].lower() in [ x.get("BundlePath", "").lower() for x in kexts if isinstance(x, dict) ]: # Already have it, skip continue # We need it, it seems kexts.append(kext) new_kexts = [] for kext in kexts: if not isinstance(kext, dict): # Not a dict - skip it continue if not kext.get("BundlePath", "").lower() in [ x[0]["BundlePath"].lower() for x in kext_list ]: # Not there, skip it continue new_kexts.append(kext) # Let's check inheritance via the info # We need to ensure that no 2 kexts consider each other as parents unordered_kexts = [] for x in new_kexts: x = next( (y for y in kext_list if y[0].get("BundlePath", "") == x.get("BundlePath", "")), None) if not x: continue parents = [ next( (z for z in new_kexts if z.get("BundlePath", "") == y[0].get("BundlePath", "")), []) for y in kext_list if y[1].get("CFBundleIdentifier", None) in x[1].get( "OSBundleLibraries", []) ] children = [ next( (z for z in new_kexts if z.get("BundlePath", "") == y[0].get("BundlePath", "")), []) for y in kext_list if x[1].get("CFBundleIdentifier", None) in y[1].get( "OSBundleLibraries", []) ] parents = [ y for y in parents if not y in children and not y.get("BundlePath", "") == x[0].get("BundlePath", "") ] unordered_kexts.append({"kext": x[0], "parents": parents}) ordered_kexts = [] disabled_parents = [] while len( unordered_kexts ): # This could be dangerous if things aren't properly prepared above kext = unordered_kexts.pop(0) if len(kext["parents"]): disabled_parents.extend([ x.get("BundlePath", "") for x in kext["parents"] if x.get("Enabled", True) == False and not x.get("BundlePath", "") in disabled_parents ]) if not all(x in ordered_kexts for x in kext["parents"]): unordered_kexts.append(kext) continue ordered_kexts.append( next(x for x in new_kexts if x.get("BundlePath", "") == kext["kext"].get( "BundlePath", ""))) # Let's compare against the original load order - to prevent mis-prompting missing_kexts = [x for x in ordered_kexts if not x in original_kexts] original_kexts.extend(missing_kexts) # Let's walk both lists and gather all kexts that are in different spots rearranged = [] while True: check1 = [ x.get("BundlePath", "") for x in ordered_kexts if not x.get("BundlePath", "") in rearranged ] check2 = [ x.get("BundlePath", "") for x in original_kexts if not x.get("BundlePath", "") in rearranged ] out_of_place = next( (x for x in range(len(check1)) if check1[x] != check2[x]), None) if out_of_place == None: break rearranged.append(check2[out_of_place]) # Verify if the load order changed - and prompt the user if need be if len(rearranged): print( "\nIncorrect kext load order has been corrected:\n\n{}".format( "\n".join(rearranged))) ordered_kexts = original_kexts # We didn't want to update it if len(disabled_parents): print("\nDisabled parent kexts have been enabled:\n\n{}".format( "\n".join(disabled_parents))) for x in ordered_kexts: # Walk our kexts and enable the parents if x.get("BundlePath", "") in disabled_parents: x["Enabled"] = True # Finally - we walk the kexts and ensure that we're not loading the same CFBundleIdentifier more than once enabled_kexts = [] duplicate_bundles = [] duplicates_disabled = [] for kext in ordered_kexts: # Check path length long_paths.extend(self.check_path_length(kext)) temp_kext = {} # Shallow copy the kext entry to avoid changing it in ordered_kexts for x in kext: temp_kext[x] = kext[x] duplicates_disabled.append(temp_kext) # Ignore if alreday disabled if not temp_kext.get("Enabled", False): continue # Get the original info info = next((x for x in kext_list if x[0].get("BundlePath", "") == temp_kext.get("BundlePath", "")), None) if not info or not info[1].get("CFBundleIdentifier", None): continue # Broken info # Let's see if it's already in enabled_kexts - and compare the Min/Max/Match Kernel options temp_min, temp_max = self.get_min_max_from_kext( temp_kext, "MatchKernel" in kext_add) # Gather a list of like IDs comp_kexts = [ x for x in enabled_kexts if x[1]["CFBundleIdentifier"] == info[1]["CFBundleIdentifier"] ] # Walk the comp_kexts, and disable if we find an overlap for comp_info in comp_kexts: comp_kext = comp_info[0] # Gather our min/max comp_min, comp_max = self.get_min_max_from_kext( comp_kext, "MatchKernel" in kext_add) # Let's see if we don't overlap if temp_min > comp_max or temp_max < comp_min: # We're good, continue continue # We overlapped - let's disable it temp_kext["Enabled"] = False # Add it to the list - then break out of this loop duplicate_bundles.append(temp_kext.get("BundlePath", "")) break # Check if we ended up disabling temp_kext, and if not - add it to the enabled_kexts list if temp_kext.get("Enabled", False): enabled_kexts.append((temp_kext, info[1])) # Check if we have duplicates - and offer to disable them if len(duplicate_bundles): print("\nDuplicate CFBundleIdentifiers have been disabled:\n\n{}". format("\n".join(duplicate_bundles))) ordered_kexts = duplicates_disabled tree_dict["Kernel"]["Add"] = ordered_kexts # Let's walk the Tools folder if it exists if not "Misc" in tree_dict or not isinstance(tree_dict["Misc"], dict): tree_dict["Misc"] = {"Tools": []} if not "Tools" in tree_dict["Misc"] or not isinstance( tree_dict["Misc"]["Tools"], list): tree_dict["Misc"]["Tools"] = [] if os.path.exists(oc_tools) and os.path.isdir(oc_tools): tools_list = [] # We need to gather a list of all the files inside that and with .efi for path, subdirs, files in os.walk(oc_tools): for name in files: if not name.startswith(".") and name.lower().endswith( ".efi"): # Save it new_tool_entry = { # "Arguments":"", # "Auxiliary":True, "Name": name, "Comment": name, "Enabled": True, "Path": os.path.join(path, name)[len(oc_tools):].replace( "\\", "/").lstrip("/") # Strip the /Volumes/EFI/ } # Add our snapshot custom entries, if any for x in tool_add: new_tool_entry[x] = tool_add[x] tools_list.append(new_tool_entry) tools = [] if clean else tree_dict["Misc"]["Tools"] for tool in sorted(tools_list, key=lambda x: x.get("Path", "").lower()): if tool["Path"].lower() in [ x.get("Path", "").lower() for x in tools if isinstance(x, dict) ]: # Already have it, skip continue # We need it, it seems tools.append(tool) new_tools = [] for tool in tools: if not isinstance(tool, dict): # Not a dict - skip it continue if not tool.get("Path", "").lower() in [ x["Path"].lower() for x in tools_list ]: # Not there, skip it continue new_tools.append(tool) # Check path length long_paths.extend(self.check_path_length(tool)) tree_dict["Misc"]["Tools"] = new_tools else: # Make sure our Tools list is empty tree_dict["Misc"]["Tools"] = [] # Last we need to walk the .efi drivers if not "UEFI" in tree_dict or not isinstance(tree_dict["UEFI"], dict): tree_dict["UEFI"] = {"Drivers": []} if not "Drivers" in tree_dict["UEFI"] or not isinstance( tree_dict["UEFI"]["Drivers"], list): tree_dict["UEFI"]["Drivers"] = [] if os.path.exists(oc_drivers) and os.path.isdir(oc_drivers): drivers_list = [] # We need to gather a list of all the files inside that and with .efi for path, subdirs, files in os.walk(oc_drivers): for name in files: if not name.startswith(".") and name.lower().endswith( ".efi"): # Check if we're using the new approach - or just listing the paths if not driver_add: drivers_list.append( os.path.join( path, name)[len(oc_drivers):].replace( "\\", "/").lstrip( "/")) # Strip the /Volumes/EFI/ else: new_driver_entry = { # "Arguments": "", "Enabled": True, "Path": os.path.join( path, name)[len(oc_drivers):].replace( "\\", "/").lstrip( "/") # Strip the /Volumes/EFI/ } # Add our snapshot custom entries, if any for x in driver_add: new_driver_entry[x] = name if x.lower( ) == "comment" else driver_add[x] drivers_list.append(new_driver_entry) drivers = [] if clean else tree_dict["UEFI"]["Drivers"] for driver in sorted(drivers_list, key=lambda x: x.get("Path", "").lower() if driver_add else x): if not driver_add: # Old way if not isinstance(driver, (str, unicode)) or driver.lower() in [ x.lower() for x in drivers if isinstance(x, (str, unicode)) ]: continue else: if driver["Path"].lower() in [ x.get("Path", "").lower() for x in drivers if isinstance(x, dict) ]: # Already have it, skip continue # We need it, it seems drivers.append(driver) new_drivers = [] for driver in drivers: if not driver_add: # Old way if not isinstance( driver, (str, unicode)) or not driver.lower() in [ x.lower() for x in drivers_list if isinstance(x, (str, unicode)) ]: continue else: if not isinstance(driver, dict): # Not a dict - skip it continue if not driver.get("Path", "").lower() in [ x["Path"].lower() for x in drivers_list ]: # Not there, skip it continue new_drivers.append(driver) # Check path length long_paths.extend(self.check_path_length(driver)) tree_dict["UEFI"]["Drivers"] = new_drivers else: # Make sure our Drivers list is empty tree_dict["UEFI"]["Drivers"] = [] # Check if we have any paths that are too long if long_paths: formatted = [] for entry in long_paths: item, name, keys = entry if isinstance(item, str): # It's an older string path formatted.append(name) elif isinstance(item, dict): formatted.append("{} -> {}".format(name, ", ".join(keys))) # Show the warning of lengthy paths print( "\nThe following exceed the {:,} character safe path max declared by OpenCore\nand may not work as intended:\n\n{}" .format(self.safe_path_length, "\n".join(formatted))) try: with open(out_file, "wb") as f: plist.dump(tree_dict, f) print("\nOutput saved to: {}".format(out_file)) except Exception as e: print("Failed to write output plist: {}".format(e)) exit(1)