Files
Opencore-Lenovo-Thinkpad-T550/ACPI/SSDTTime-master/PatchMerge.py
2025-10-17 16:40:24 +02:00

587 lines
25 KiB
Python

from Scripts import utils, plist
import argparse, os
class PatchMerge:
def __init__(self, config=None, results=None, overwrite=False, interactive=True):
self.u = utils.Utils("Patch Merge")
self.w = 80
self.h = 24
self.red = "\u001b[41;1m"
self.yel = "\u001b[43;1m"
self.grn = "\u001b[42;1m"
self.blu = "\u001b[46;1m"
self.rst = "\u001b[0m"
self.copy_as_path = self.u.check_admin() if os.name=="nt" else False
if 2/3==0:
# ANSI escapes don't seem to work properly with python 2.x
self.red = self.yel = self.grn = self.blu = self.rst = ""
if os.name == "nt":
if 2/3!=0:
os.system("color") # Allow ANSI color escapes.
self.w = 120
self.h = 30
self.interactive = interactive
self.overwrite = overwrite
self.target_patches = (
("OpenCore","patches_OC.plist"),
("Clover","patches_Clover.plist")
)
self.config_path = config
self.config_type = None
self.output = results or self.get_default_results_folder()
# Expand paths as needed
if self.config_path:
self.config_path = os.path.realpath(self.config_path)
self.config_type,_,_ = self.get_plist_info(self.config_path)
if self.output:
self.output = os.path.realpath(self.output)
def _get_patches_plists(self, path):
# Append patches_OC/Clover.plist to the path, and return a list
# with the format:
# ((oc_path,exists,plist_name),(clover_path,exists,plist_name))
path_checks = []
for p_type,name in self.target_patches:
if path:
p = os.path.join(path,name)
isfile = os.path.isfile(p)
else:
p = None
isfile = False
path_checks.append((
p,
isfile,
name
))
return path_checks
def get_default_results_folder(self, prompt=False):
# Let's attempt to locate a Results folder either in the same
# directory as this script, or in the parent directory.
# If none is found - we'll have to prompt the user as needed.
#
# Try our directory first
local_path = os.path.dirname(os.path.realpath(__file__))
local_results = os.path.join(local_path,"Results")
parent_results = os.path.realpath(os.path.join(local_path,"..","Results"))
potentials = []
for path in (local_results,parent_results):
if os.path.isdir(path):
# Check if we have the files we need
o,c = self._get_patches_plists(path)
if o[1] or c[1]:
potentials.append(path)
if potentials:
return potentials[0]
# If we got here - we didn't find anything - check if we need
# to prompt
if not prompt:
# Nope - bail
return None
# We're prompting
return self.select_results_folder()
def select_results_folder(self):
while True:
self.u.head("Select Results Folder")
print("")
if self.copy_as_path:
print("NOTE: Currently running as admin on Windows - drag and drop may not work.")
print(" Shift + right-click in Explorer and select 'Copy as path' then paste here instead.")
print("")
print("M. Main Menu")
print("Q. Quit")
print("")
print("NOTE: This is the folder containing the patches_OC.plist and")
print(" patches_Clover.plist you are trying to merge. It will also be where")
print(" the patched config.plist is saved.")
print("")
path = self.u.grab("Please drag and drop the Results folder here: ")
if not path:
continue
if path.lower() == "m":
return self.output
elif path.lower() == "q":
self.u.custom_quit()
test_path = self.u.check_path(path)
if os.path.isfile(test_path):
# Got a file - get the containing folder
test_path = os.path.dirname(test_path)
if not test_path:
self.u.head("Invalid Path")
print("")
print("That path either does not exist, or is not a folder.")
print("")
self.u.grab("Press [enter] to return...")
continue
# Got a folder - check for patches_OC/Clover.plist
o,c = self._get_patches_plists(test_path)
if not (o[1] or c[1]):
# No patches plists in there
self.u.head("Missing Files")
print("")
print("Neither patches_OC.plist nor patches_Clover.plist were found at that path.")
print("")
self.u.grab("Press [enter] to return...")
continue
# We got what we need - set and return the path
self.output = test_path
return self.output
def get_ascii_print(self, data):
# Helper to sanitize unprintable characters by replacing them with
# ? where needed
unprintables = False
all_zeroes = True
ascii_string = ""
for b in data:
if not isinstance(b,int):
try: b = ord(b)
except: pass
if b != 0:
# Not wildcard matching
all_zeroes = False
if ord(" ") <= b < ord("~"):
ascii_string += chr(b)
else:
ascii_string += "?"
unprintables = True
return (False if all_zeroes else unprintables,ascii_string)
def check_normalize(self, patch_or_drop, normalize_headers, check_type="Patch"):
sig = ("OemTableId","TableSignature")
if normalize_headers:
# OpenCore - and NormalizeHeaders is enabled. Check if we have
# any unprintable ASCII chars in our OemTableId or TableSignature
# and warn.
if any(self.get_ascii_print(plist.extract_data(patch_or_drop.get(x,b"\x00")))[0] for x in sig):
print("\n{}!! WARNING !!{} NormalizeHeaders is {}ENABLED{}, and table ids contain unprintable".format(
self.yel,
self.rst,
self.grn,
self.rst
))
print(" characters! {} may not match or apply!\n".format(check_type))
return True
else:
# Not enabled - check for question marks as that may imply characters
# were sanitized when creating the patches/dropping tables.
if any(b"\x3F" in plist.extract_data(patch_or_drop.get(x,b"\x00")) for x in sig):
print("\n{}!! WARNING !!{} NormalizeHeaders is {}DISABLED{}, and table ids contain '?'!".format(
self.yel,
self.rst,
self.red,
self.rst
))
print(" {} may not match or apply!\n".format(check_type))
return True
return False
def ensure_path(self, plist_data, path_list, final_type = list):
if not path_list:
return plist_data
if not isinstance(plist_data,dict):
plist_data = {} # Override it with a dict
# Set our initial reference, then iterate the
# path list
last = plist_data
for i,path in enumerate(path_list,start=1):
# Check if our next path var is in last
if not path in last:
last[path] = {} if i < len(path_list) else final_type()
# Make sure it's the correct type if we're at the
# end of the entries
if i >= len(path_list) and not isinstance(last[path],final_type):
# Override it
last[path] = final_type()
# Update our reference
last = last[path]
return plist_data
def get_unique_name(self,name,target_folder,name_append=""):
# Get a new file name in the target folder so we don't override the original
name = os.path.basename(name)
ext = "" if not "." in name else name.split(".")[-1]
if ext: name = name[:-len(ext)-1]
if name_append: name = name+str(name_append)
check_name = ".".join((name,ext)) if ext else name
if not os.path.exists(os.path.join(target_folder,check_name)):
return check_name
# We need a unique name
num = 1
while True:
check_name = "{}-{}".format(name,num)
if ext: check_name += "."+ext
if not os.path.exists(os.path.join(target_folder,check_name)):
return check_name
num += 1 # Increment our counter
def pause_interactive(self, return_value=None):
if self.interactive:
print("")
self.u.grab("Press [enter] to return...")
return return_value
def patch_plist(self):
# Retain the config name
if self.interactive:
self.u.head("Patching Plist")
print("")
# Make sure we have a config_path
if not self.config_path:
print("No target plist path specified!")
return self.pause_interactive()
# Make sure that config_path exists
if not os.path.isfile(self.config_path):
print("Could not locate target plist at:")
print(" - {}".format(self.config_path))
return self.pause_interactive()
# Make sure our output var has a value
if not self.output:
print("No Results folder path specified!")
return self.pause_interactive()
config_name = os.path.basename(self.config_path)
print("Loading {}...".format(config_name))
self.config_type,config_data,e = self.get_plist_info(self.config_path)
if e:
print(" - Failed to load! {}".format(e))
return self.pause_interactive()
# Recheck the config.plist type
if not self.config_type:
print("Could not determine plist type!")
return self.pause_interactive()
# Ensure our patches plists exist, and break out info
# into the target_path and target_name as needed
target_path,_,target_name = self.get_patch_plist_for_type(
self.output,
self.config_type
)
# This should only show up if output is None/False/empty
if not target_path:
print("Could not locate {} in:".format(target_name or "the required patches plist"))
print(" - {}".format(self.output))
return self.pause_interactive()
# Make sure the path actually exists - and is a file
if not os.path.isfile(target_path):
print("Could not locate required patches at:")
print(" - {}".format(target_path))
return self.pause_interactive()
# Set up some preliminary variables for reporting later
errors_found = normalize_headers = False # Default to off
target_name = os.path.basename(target_path)
print("Loading {}...".format(target_name))
# Load the target plist
_,target_data,e = self.get_plist_info(target_path)
if e:
print(" - Failed to load! {}".format(e))
return self.pause_interactive()
print("Ensuring paths in {} and {}...".format(config_name,target_name))
# Make sure all the needed values are there
if self.config_type == "OpenCore":
for p in (("ACPI","Add"),("ACPI","Delete"),("ACPI","Patch")):
print(" - {}...".format(" -> ".join(p)))
config_data = self.ensure_path(config_data,p)
target_data = self.ensure_path(target_data,p)
print(" - ACPI -> Quirks...")
config_data = self.ensure_path(config_data,("ACPI","Quirks"),final_type=dict)
normalize_headers = config_data["ACPI"]["Quirks"].get("NormalizeHeaders",False)
if not isinstance(normalize_headers,(bool)):
errors_found = True
print("\n{}!! WARNING !!{} ACPI -> Quirks -> NormalizeHeaders is malformed - assuming False".format(
self.yel,
self.rst
))
normalize_headers = False
# Set up our patch sources
ssdts = target_data["ACPI"]["Add"]
patch = target_data["ACPI"]["Patch"]
drops = target_data["ACPI"]["Delete"]
# Set up our original values
s_orig = config_data["ACPI"]["Add"]
p_orig = config_data["ACPI"]["Patch"]
d_orig = config_data["ACPI"]["Delete"]
else:
for p in (("ACPI","DropTables"),("ACPI","SortedOrder"),("ACPI","DSDT","Patches")):
print(" - {}...".format(" -> ".join(p)))
config_data = self.ensure_path(config_data,p)
target_data = self.ensure_path(target_data,p)
# Set up our patch sources
ssdts = target_data["ACPI"]["SortedOrder"]
patch = target_data["ACPI"]["DSDT"]["Patches"]
drops = target_data["ACPI"]["DropTables"]
# Set up our original values
s_orig = config_data["ACPI"]["SortedOrder"]
p_orig = config_data["ACPI"]["DSDT"]["Patches"]
d_orig = config_data["ACPI"]["DropTables"]
print("")
if not ssdts:
print("--- No SSDTs to add - skipping...")
else:
print("--- Walking target SSDTs ({:,} total)...".format(len(ssdts)))
s_rem = []
# Gather any entries broken from user error
s_broken = [x for x in s_orig if not isinstance(x,dict)] if self.config_type == "OpenCore" else []
for s in ssdts:
if self.config_type == "OpenCore":
print(" - Checking {}...".format(s["Path"]))
existing = [x for x in s_orig if isinstance(x,dict) and x["Path"] == s["Path"]]
else:
print(" - Checking {}...".format(s))
existing = [x for x in s_orig if x == s]
if existing:
print(" --> Located {:,} existing to replace...".format(len(existing)))
s_rem.extend(existing)
if s_rem:
print(" - Removing {:,} existing duplicate{}...".format(len(s_rem),"" if len(s_rem)==1 else "s"))
for r in s_rem:
if r in s_orig: s_orig.remove(r)
else:
print(" - No duplicates to remove...")
print(" - Adding {:,} SSDT{}...".format(len(ssdts),"" if len(ssdts)==1 else "s"))
s_orig.extend(ssdts)
if s_broken:
errors_found = True
print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format(
self.yel,
self.rst,
len(s_broken),
"y" if len(d_broken)==1 else "ies",
config_name
))
print("")
if not patch:
print("--- No patches to add - skipping...")
else:
print("--- Walking target patches ({:,} total)...".format(len(patch)))
p_rem = []
# Gather any entries broken from user error
p_broken = [x for x in p_orig if not isinstance(x,dict)]
for p in patch:
print(" - Checking {}...".format(p["Comment"]))
if self.config_type == "OpenCore" and self.check_normalize(p,normalize_headers):
errors_found = True
existing = [x for x in p_orig if isinstance(x,dict) and x["Find"] == p["Find"] and x["Replace"] == p["Replace"]]
if existing:
print(" --> Located {:,} existing to replace...".format(len(existing)))
p_rem.extend(existing)
# Remove any dupes
if p_rem:
print(" - Removing {:,} existing duplicate{}...".format(len(p_rem),"" if len(p_rem)==1 else "s"))
for r in p_rem:
if r in p_orig: p_orig.remove(r)
else:
print(" - No duplicates to remove...")
print(" - Adding {:,} patch{}...".format(len(patch),"" if len(patch)==1 else "es"))
p_orig.extend(patch)
if p_broken:
errors_found = True
print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format(
self.yel,
self.rst,
len(p_broken),
"y" if len(d_broken)==1 else "ies",
config_name
))
print("")
if not drops:
print("--- No tables to drop - skipping...")
else:
print("--- Walking target tables to drop ({:,} total)...".format(len(drops)))
d_rem = []
# Gather any entries broken from user error
d_broken = [x for x in d_orig if not isinstance(x,dict)]
for d in drops:
if self.config_type == "OpenCore":
print(" - Checking {}...".format(d["Comment"]))
if self.check_normalize(d,normalize_headers,check_type="Dropped table"):
errors_found = True
existing = [x for x in d_orig if isinstance(x,dict) and x["TableSignature"] == d["TableSignature"] and x["OemTableId"] == d["OemTableId"]]
else:
name = " - ".join([x for x in (d.get("Signature",""),d.get("TableId","")) if x]) or "Unknown Dropped Table"
print(" - Checking {}...".format(name))
existing = [x for x in d_orig if isinstance(x,dict) and x.get("Signature") == d.get("Signature") and x.get("TableId") == d.get("TableId")]
if existing:
print(" --> Located {:,} existing to replace...".format(len(existing)))
d_rem.extend(existing)
if d_rem:
print(" - Removing {:,} existing duplicate{}...".format(len(d_rem),"" if len(d_rem)==1 else "s"))
for r in d_rem:
if r in d_orig: d_orig.remove(r)
else:
print(" - No duplicates to remove...")
print(" - Dropping {:,} table{}...".format(len(drops),"" if len(drops)==1 else "s"))
d_orig.extend(drops)
if d_broken:
errors_found = True
print("\n{}!! WARNING !!{} {:,} Malformed entr{} found - please fix your {}!".format(
self.yel,
self.rst,
len(d_broken),
"y" if len(d_broken)==1 else "ies",
config_name
))
print("")
if self.overwrite:
output_path = self.config_path
else:
config_name = self.get_unique_name(config_name,self.output)
output_path = os.path.join(self.output,config_name)
print("Saving to {}...".format(output_path))
try:
plist.dump(config_data,open(output_path,"wb"))
except Exception as e:
print(" - Failed to save! {}".format(e))
return self.pause_interactive()
print(" - Saved.")
print("")
if errors_found:
print("{}!! WARNING !!{} Potential errors were found when merging - please address them!".format(
self.yel,
self.rst
))
print("")
if not self.overwrite:
print("{}!! WARNING !!{} Make sure you review the saved {} before replacing!".format(
self.red,
self.rst,
config_name
))
print("")
print("Done.")
return self.pause_interactive()
def get_plist_info(self, config_path):
# Attempts to load the passed config and return a tuple
# of (type_string,config_data,error)
type_string = config_data = e = None
try:
config_data = plist.load(open(config_path,"rb"))
except Exception as e:
return (None,None,e)
if not isinstance(config_data,dict):
e = "Invalid root node type: {}".format(type(config_data))
else:
type_string = "OpenCore" if "PlatformInfo" in config_data else "Clover" if "SMBIOS" in config_data else None
return (type_string,config_data,None)
def get_patch_plist_for_type(self, path, config_type):
o,c = self._get_patches_plists(path)
return {
"OpenCore":o,
"Clover":c
}.get(config_type,(None,False,None))
def select_plist(self):
while True:
self.u.head("Select Plist")
print("")
if self.copy_as_path:
print("NOTE: Currently running as admin on Windows - drag and drop may not work.")
print(" Shift + right-click in Explorer and select 'Copy as path' then paste here instead.")
print("")
print("M. Main Menu")
print("Q. Quit")
print("")
path = self.u.grab("Please drag and drop the config.plist here: ")
if not path: continue
if path.lower() == "m": return
elif path.lower() == "q": self.u.custom_quit()
test_path = self.u.check_path(path)
if not test_path or not os.path.isfile(test_path):
self.u.head("Invalid Path")
print("")
print("That path either does not exist, or is not a file.")
print("")
self.u.grab("Press [enter] to return...")
continue
# Got a file - try to load it
t,_,e = self.get_plist_info(test_path)
if e:
self.u.head("Invalid File")
print("")
print("That file failed to load:\n\n{}".format(e))
print("")
self.u.grab("Press [enter] to return...")
continue
# Got a valid file
self.config_path = test_path
self.config_type = t
return
def main(self):
# Gather some preliminary info for display
target_path,target_exists,target_name = self.get_patch_plist_for_type(
self.output,
self.config_type
)
self.u.resize(self.w,self.h)
self.u.head()
print("")
print("Current config.plist: {}".format(self.config_path))
print("Type of config.plist: {}".format(self.config_type or "Unknown"))
print("Results Folder: {}".format(self.output))
print("Patches Plist: {}{}".format(
target_name or "Unknown",
"" if (not target_name or target_exists) else " - {}!! MISSING !!{}".format(self.red,self.rst)
))
print("Overwrite Original: {}{}{}{}".format(
self.red if self.overwrite else self.grn,
"!! True !!" if self.overwrite else "False",
self.rst,
" - Make Sure You Have A Backup!" if self.overwrite else ""
))
print("")
print("C. Select config.plist")
print("O. Toggle Overwrite Original")
print("R. Select Results Folder")
if self.config_path and target_exists:
print("P. Patch with {}".format(target_name))
print("")
print("Q. Quit")
print("")
menu = self.u.grab("Please make a selection: ")
if not len(menu):
return
if menu.lower() == "q":
self.u.custom_quit()
elif menu.lower() == "c":
self.select_plist()
elif menu.lower() == "o":
self.overwrite ^= True
elif menu.lower() == "r":
self.select_results_folder()
elif menu.lower() == "p" and self.config_path and target_exists:
self.patch_plist()
if __name__ == '__main__':
# Setup the cli args
parser = argparse.ArgumentParser(prog="PatchMerge.py", description="PatchMerge - py script to merge patches_[OC/Clover].plist with a config.plist.")
parser.add_argument("-c", "--config", help="path to target config.plist - required if running in non-interactive mode")
parser.add_argument("-r", "--results", help="path to Results folder containing patches_[OC/Clover].plist - required if running in non-interactive mode")
parser.add_argument("-o", "--overwrite", help="overwrite the original config.plist", action="store_true")
parser.add_argument("-i", "--no-interaction", help="run in non-interactive mode - requires -c and -r", action="store_true")
args = parser.parse_args()
p = PatchMerge(
config=args.config,
results=args.results,
overwrite=args.overwrite,
interactive=not args.no_interaction
)
if args.no_interaction:
# We're in non-interactive mode here
p.patch_plist()
else:
# Interactive mode
if 2/3 == 0:
input = raw_input
while True:
try:
p.main()
except Exception as e:
print("An error occurred: {}".format(e))
print("")
input("Press [enter] to continue...")