From 9018aa6e5b4e5fcb7e337ba20ecb4fdeb4552c2b Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 01:50:58 +0100 Subject: [PATCH 1/8] Allow to filter versions by build number using --build option --- gibMacOS.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/gibMacOS.py b/gibMacOS.py index 4ae7897..bcd38ef 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -541,8 +541,8 @@ class gibMacOS: return print("{} not found".format(prod)) - def get_for_version(self, vers, dmg = False): - self.u.head("Downloading for {}".format(vers)) + def get_for_version(self, vers, build = None, dmg = False): + self.u.head("Downloading for {} {}".format(vers, build or "")) print("") # Map the versions to their names v = self.version_names.get(vers.lower(),vers.lower()) @@ -551,6 +551,8 @@ class gibMacOS: v_dict[self.version_names[n]] = n n = v_dict.get(v, v) for p in sorted(self.mac_prods, key=lambda x:x['version'], reverse=True): + if build and p["build"] != build: + continue pt = p["title"].lower() pv = p["version"].lower() # Need to compare verisons - n = name, v = version @@ -575,11 +577,11 @@ class gibMacOS: if (n in pt) and not len(name_match): self.download_prod(p, dmg) return - print("'{}' not found".format(vers)) + print("'{}' '{}' not found".format(vers, build)) if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument("-l", "--latest", help="downloads the version available in the current catalog (overrides --version and --product)", action="store_true") + parser.add_argument("-l", "--latest", help="downloads the version available in the current catalog (overrides --build, --version and --product)", action="store_true") parser.add_argument("-r", "--recovery", help="looks for RecoveryHDUpdate.pkg and RecoveryHDMetaDmg.pkg in lieu of com.apple.mpkg.OSInstall (overrides --dmg)", action="store_true") parser.add_argument("-d", "--dmg", help="downloads only the .dmg files", action="store_true") parser.add_argument("-s", "--savelocal", help="uses a locally saved sucatalog.plist if exists", action="store_true") @@ -587,6 +589,7 @@ if __name__ == '__main__': parser.add_argument("-c", "--catalog", help="sets the CATALOG to use - publicrelease, public, customer, developer") parser.add_argument("-p", "--product", help="sets the product id to search for (overrides --version)") parser.add_argument("-v", "--version", help="sets the version of macOS to target - eg '-v 10.14' or '-v Yosemite'") + parser.add_argument("-b", "--build", help="sets the build of macOS to target - eg '22G120' (must be used together with --version)") parser.add_argument("-m", "--maxos", help="sets the max macOS version to consider when building the url - eg 10.14") parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true") args = parser.parse_args() @@ -625,7 +628,7 @@ if __name__ == '__main__': g.get_for_product(args.product, args.dmg) exit() if args.version != None: - g.get_for_version(args.version, args.dmg) + g.get_for_version(args.version, args.build, args.dmg) exit() while True: From a93490cf99c55917b43d7bdb4d6fd196dc0d223e Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 02:37:34 +0100 Subject: [PATCH 2/8] Allow to filter products by Device ID using --device-id option This allows to easily filter only builds supporting x86_64. --- gibMacOS.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/gibMacOS.py b/gibMacOS.py index bcd38ef..fd395f7 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -224,7 +224,13 @@ class gibMacOS: name = re.search(r"(.+?)",dist_file).group(1) except: pass - return (build,version,name) + try: + # XXX: This is parsing a JavaScript array from the script part of the dist file. + device_ids = re.search(r"var supportedDeviceIDs\s*=\s*\[([^]]+)\];", dist_file)[1] + device_ids = set(i.lower() for i in re.findall(r"'([^',]+)'", device_ids)) + except: + device_ids = set() + return (build,version,name,device_ids) def get_dict_for_prods(self, prods, plist_dict = None): if plist_dict==self.catalog_data==None: @@ -262,8 +268,8 @@ class gibMacOS: prodd["packages"] = plist_dict.get("Products",{}).get(prod,{}).get("Packages",[]) # Get size prodd["size"] = self.d.get_size(sum([i["Size"] for i in prodd["packages"]])) - # Attempt to get the build/version info from the dist - prodd["build"],v,n = self.get_build_version(plist_dict.get("Products",{}).get(prod,{}).get("Distributions",{})) + # Attempt to get the build/version/name/device-ids info from the dist + prodd["build"],v,n,prodd["device_ids"] = self.get_build_version(plist_dict.get("Products",{}).get(prod,{}).get("Distributions",{})) prodd["title"] = smd.get("localization",{}).get("English",{}).get("title",n) print(" -->{}. {} ({}){}".format( str(len(prod_list)+1).rjust(3), @@ -527,10 +533,18 @@ class gibMacOS: return self.download_prod(self.mac_prods[menu-1], dmg) - def get_latest(self, dmg = False): + def get_latest(self, device_id = None, dmg = False): self.u.head("Downloading Latest") print("") - self.download_prod(sorted(self.mac_prods, key=lambda x:x['version'], reverse=True)[0], dmg) + prods = sorted(self.mac_prods, key=lambda x:x['version'], reverse=True) + if device_id: + prod = next(p for p in prods if device_id.lower() in p["device_ids"]) + if not prod: + print("No version found for Device ID '{}'".format(device_id)) + return + else: + prod = prods[0] + self.download_prod(prod, dmg) def get_for_product(self, prod, dmg = False): self.u.head("Downloading for {}".format(prod)) @@ -541,7 +555,7 @@ class gibMacOS: return print("{} not found".format(prod)) - def get_for_version(self, vers, build = None, dmg = False): + def get_for_version(self, vers, build = None, device_id = None, dmg = False): self.u.head("Downloading for {} {}".format(vers, build or "")) print("") # Map the versions to their names @@ -553,6 +567,8 @@ class gibMacOS: for p in sorted(self.mac_prods, key=lambda x:x['version'], reverse=True): if build and p["build"] != build: continue + if device_id and device_id.lower() not in p["device_ids"]: + continue pt = p["title"].lower() pv = p["version"].lower() # Need to compare verisons - n = name, v = version @@ -591,6 +607,7 @@ if __name__ == '__main__': parser.add_argument("-v", "--version", help="sets the version of macOS to target - eg '-v 10.14' or '-v Yosemite'") parser.add_argument("-b", "--build", help="sets the build of macOS to target - eg '22G120' (must be used together with --version)") parser.add_argument("-m", "--maxos", help="sets the max macOS version to consider when building the url - eg 10.14") + parser.add_argument("-D", "--device-id", help="use with --version or --latest to search for versions supporting the specified Device ID - eg VMM-x86_64 for any x86_64") parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true") args = parser.parse_args() @@ -622,13 +639,13 @@ if __name__ == '__main__': g.set_prods() if args.latest: - g.get_latest(args.dmg) + g.get_latest(device_id=args.device_id, dmg=args.dmg) exit() if args.product != None: g.get_for_product(args.product, args.dmg) exit() if args.version != None: - g.get_for_version(args.version, args.build, args.dmg) + g.get_for_version(args.version, args.build, device_id=args.device_id, dmg=args.dmg) exit() while True: From 1e82ba0813e72c8d96842f1ce3e974c89d679250 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 21:36:57 +0100 Subject: [PATCH 3/8] Add support for running in non-interactive mode - Exits with non-zero exit status on errors. - Does not hang on prompts. - Just prints text without fancy terminal stuff. --- Scripts/utils.py | 14 ++++-- gibMacOS.py | 109 ++++++++++++++++++++++++++++------------------- 2 files changed, 76 insertions(+), 47 deletions(-) diff --git a/Scripts/utils.py b/Scripts/utils.py index 08a453f..95dfcc2 100755 --- a/Scripts/utils.py +++ b/Scripts/utils.py @@ -9,8 +9,9 @@ else: class Utils: - def __init__(self, name = "Python Script"): + def __init__(self, name = "Python Script", interactive = True): self.name = name + self.interactive = interactive # Init our colors before we need to print anything cwd = os.getcwd() os.chdir(os.path.dirname(os.path.realpath(__file__))) @@ -138,6 +139,8 @@ class Utils: # returning the result timeout = kwargs.get("timeout", 0) default = kwargs.get("default", None) + if not self.interactive: + return default # If we don't have a timeout - then skip the timed sections if timeout <= 0: if sys.version_info >= (3, 0): @@ -170,11 +173,13 @@ class Utils: return default def cls(self): - os.system('cls' if os.name=='nt' else 'clear') + if not self.interactive: + return + os.system('cls' if os.name=='nt' else 'clear') def cprint(self, message, **kwargs): strip_colors = kwargs.get("strip_colors", False) - if os.name == "nt": + if os.name == "nt" or not self.interactive: strip_colors = True reset = u"\u001b[0m" # Requires sys import @@ -216,6 +221,9 @@ class Utils: # Header drawing method def head(self, text = None, width = 55): + if not self.interactive: + print(text) + return if text == None: text = self.name self.cls() diff --git a/gibMacOS.py b/gibMacOS.py index fd395f7..6381fac 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -2,10 +2,17 @@ from Scripts import downloader,utils,run,plist import os, shutil, time, sys, argparse, re, json +class ProgramError(Exception): + def __init__(self, message, title = "Error"): + super().__init__(message) + self.title = title + + class gibMacOS: - def __init__(self): + def __init__(self, interactive = True): + self.interactive = interactive self.d = downloader.Downloader() - self.u = utils.Utils("gibMacOS") + self.u = utils.Utils("gibMacOS", interactive=interactive) self.r = run.Run() self.min_w = 80 self.min_h = 24 @@ -73,6 +80,8 @@ class gibMacOS: ) def resize(self, width=0, height=0): + if not self.interactive: + return width = width if width > self.min_w else self.min_w height = height if height > self.min_h else self.min_h self.u.resize(width, height) @@ -84,22 +93,18 @@ class gibMacOS: try: json.dump(self.settings,open(self.settings_path,"w"),indent=2) except Exception as e: - self.u.head("Error Saving Settings") - print("") - print("Failed to save settings to:\n\n{}\n\nWith error:\n\n - {}\n".format(self.settings_path,repr(e))) - self.u.grab("Press [enter] to continue...") + raise ProgramError( + "Failed to save settings to:\n\n{}\n\nWith error:\n\n - {}\n".format(self.settings_path,repr(e)), + title="Error Saving Settings") def set_prods(self): self.resize() if not self.get_catalog_data(self.save_local): - self.u.head("Catalog Data Error") - print("") - print("The currently selected catalog ({}) was not reachable".format(self.current_catalog)) + message += "The currently selected catalog ({}) was not reachable\n".format(self.current_catalog) if self.save_local: - print("and I was unable to locate a valid {} file in the\n{} directory.".format(self.plist, self.scripts)) - print("Please ensure you have a working internet connection.") - print("") - self.u.grab("Press [enter] to exit...") + message += "and I was unable to locate a valid {} file in the\n{} directory.\n".format(self.plist, self.scripts) + message += "Please ensure you have a working internet connection." + raise ProgramError(message, title="Catalog Data Error") self.u.head("Parsing Data") print("") print("Scanning products after catalog download...\n") @@ -297,12 +302,7 @@ class gibMacOS: # add it to the list dl_list.append(x["URL"]) if not len(dl_list): - self.u.head("Error") - print("") - print("There were no files to download") - print("") - self.u.grab("Press [enter] to return...") - return + raise ProgramError("There were no files to download") c = 0 done = [] if self.print_urls: @@ -310,8 +310,9 @@ class gibMacOS: print("") print("{}:\n".format(name)) print("\n".join([" - {} \n --> {}".format(os.path.basename(x), x) for x in dl_list])) - print("") - self.u.grab("Press [enter] to return...") + if self.interactive: + print("") + self.u.grab("Press [enter] to return...") return # Only check the dirs if we need to cwd = os.getcwd() @@ -322,6 +323,8 @@ class gibMacOS: print("") print("It looks like you've already downloaded {}".format(name)) print("") + if not self.interactive: + return menu = self.u.grab("Redownload? (y/n): ") if not len(menu): continue @@ -371,7 +374,10 @@ class gibMacOS: print("Files saved to:") print(" {}".format(os.path.join(os.getcwd(), self.saves, self.current_catalog, name))) print("") - self.u.grab("Press [enter] to return...") + if self.interactive: + self.u.grab("Press [enter] to return...") + elif len(failed): + raise ProgramError("{} files failed to download".format(len(failed))) def show_catalog_url(self): self.resize() @@ -381,9 +387,9 @@ class gibMacOS: print("Max macOS Version: {}".format(self.num_to_macos(self.current_macos,for_url=False))) print("") print("{}".format(self.build_url())) - print("") - menu = self.u.grab("Press [enter] to return...") - return + if self.interactive: + print("") + self.u.grab("Press [enter] to return...") def pick_catalog(self): self.resize() @@ -540,8 +546,7 @@ class gibMacOS: if device_id: prod = next(p for p in prods if device_id.lower() in p["device_ids"]) if not prod: - print("No version found for Device ID '{}'".format(device_id)) - return + raise ProgramError("No version found for Device ID '{}'".format(device_id)) else: prod = prods[0] self.download_prod(prod, dmg) @@ -553,7 +558,7 @@ class gibMacOS: if p["product"] == prod: self.download_prod(p, dmg) return - print("{} not found".format(prod)) + raise ProgramError("{} not found".format(prod)) def get_for_version(self, vers, build = None, device_id = None, dmg = False): self.u.head("Downloading for {} {}".format(vers, build or "")) @@ -593,7 +598,7 @@ class gibMacOS: if (n in pt) and not len(name_match): self.download_prod(p, dmg) return - print("'{}' '{}' not found".format(vers, build)) + raise ProgramError("'{}' '{}' not found".format(vers, build or "")) if __name__ == '__main__': parser = argparse.ArgumentParser() @@ -609,9 +614,10 @@ if __name__ == '__main__': parser.add_argument("-m", "--maxos", help="sets the max macOS version to consider when building the url - eg 10.14") parser.add_argument("-D", "--device-id", help="use with --version or --latest to search for versions supporting the specified Device ID - eg VMM-x86_64 for any x86_64") parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true") + parser.add_argument("--no-interactive", help="run in non-interactive mode", action="store_true") args = parser.parse_args() - g = gibMacOS() + g = gibMacOS(interactive=not args.no_interactive) if args.recovery: args.dmg = False g.find_recovery = args.recovery @@ -635,18 +641,33 @@ if __name__ == '__main__': # Set the catalog g.set_catalog(args.catalog) - # Done setting up pre-requisites - g.set_prods() + try: + # Done setting up pre-requisites + g.set_prods() - if args.latest: - g.get_latest(device_id=args.device_id, dmg=args.dmg) - exit() - if args.product != None: - g.get_for_product(args.product, args.dmg) - exit() - if args.version != None: - g.get_for_version(args.version, args.build, device_id=args.device_id, dmg=args.dmg) - exit() - - while True: - g.main(args.dmg) + if args.latest: + g.get_latest(device_id=args.device_id, dmg=args.dmg) + elif args.product != None: + g.get_for_product(args.product, args.dmg) + elif args.version != None: + g.get_for_version(args.version, args.build, device_id=args.device_id, dmg=args.dmg) + elif g.interactive: + while True: + try: + g.main(args.dmg) + except ProgramError as e: + g.u.head(e.title) + print("") + print(str(e)) + print("") + g.u.grab("Press [enter] to return...") + else: + raise ProgramError("No command specified") + except ProgramError as e: + print(str(e)) + if g.interactive: + print("") + g.u.grab("Press [enter] to exit...") + else: + exit(1) + exit(0) From dfc6e2feb8c3de58874fbac55552843793c50af7 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 21:49:47 +0100 Subject: [PATCH 4/8] Deduplicate printing empty line after head --- Scripts/utils.py | 1 + gibMacOS.py | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/Scripts/utils.py b/Scripts/utils.py index 95dfcc2..1953c1b 100755 --- a/Scripts/utils.py +++ b/Scripts/utils.py @@ -239,6 +239,7 @@ class Utils: middle = middle[:-di] + "...#" print(middle) print("#"*width) + print("") def resize(self, width, height): print('\033[8;{};{}t'.format(height, width)) diff --git a/gibMacOS.py b/gibMacOS.py index 6381fac..5dd6bbe 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -106,7 +106,6 @@ class gibMacOS: message += "Please ensure you have a working internet connection." raise ProgramError(message, title="Catalog Data Error") self.u.head("Parsing Data") - print("") print("Scanning products after catalog download...\n") self.mac_prods = self.get_dict_for_prods(self.get_installers()) @@ -151,7 +150,6 @@ class gibMacOS: # 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() @@ -307,7 +305,6 @@ class gibMacOS: done = [] if self.print_urls: self.u.head("Download Links") - print("") print("{}:\n".format(name)) print("\n".join([" - {} \n --> {}".format(os.path.basename(x), x) for x in dl_list])) if self.interactive: @@ -320,7 +317,6 @@ class gibMacOS: if os.path.exists(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)): while True: self.u.head("Already Exists") - print("") print("It looks like you've already downloaded {}".format(name)) print("") if not self.interactive: @@ -339,7 +335,6 @@ class gibMacOS: for x in dl_list: c += 1 self.u.head("Downloading File {} of {}".format(c, len(dl_list))) - print("") if len(done): print("\n".join(["{} --> {}".format(y["name"], "Succeeded" if y["status"] else "Failed") for y in done])) print("") @@ -356,7 +351,6 @@ class gibMacOS: succeeded = [x for x in done if x["status"]] failed = [x for x in done if not x["status"]] self.u.head("Downloaded {} of {}".format(len(succeeded), len(dl_list))) - print("") print("Succeeded:") if len(succeeded): for x in succeeded: @@ -382,7 +376,6 @@ class gibMacOS: def show_catalog_url(self): self.resize() self.u.head() - print("") print("Current Catalog: {}".format(self.current_catalog)) print("Max macOS Version: {}".format(self.num_to_macos(self.current_macos,for_url=False))) print("") @@ -394,7 +387,6 @@ class gibMacOS: def pick_catalog(self): self.resize() self.u.head("Select SU Catalog") - print("") count = 0 for x in self.catalog_suffix: count += 1 @@ -427,7 +419,6 @@ class gibMacOS: def pick_macos(self): self.resize() self.u.head("Select Max macOS Version") - print("") print("Currently set to {}".format(self.num_to_macos(self.current_macos,for_url=False))) print("") print("M. Main Menu") @@ -501,7 +492,6 @@ class gibMacOS: elif menu[0].lower() == "l" and sys.platform.lower() == "darwin": # Clear the software update catalog self.u.head("Clearing SU CatalogURL") - print("") print("sudo softwareupdate --clear-catalog") self.r.run({"args":["softwareupdate","--clear-catalog"],"sudo":True}) print("") @@ -510,7 +500,6 @@ class gibMacOS: elif menu[0].lower() == "s" and sys.platform.lower() == "darwin": # Set the software update catalog to our current catalog url self.u.head("Setting SU CatalogURL") - print("") url = self.build_url(catalog=self.current_catalog, version=self.current_macos) print("Setting catalog URL to:\n{}".format(url)) print("") @@ -525,7 +514,6 @@ class gibMacOS: if menu[0].lower() in ["m","c","r"]: self.resize() self.u.head("Parsing Data") - print("") print("Re-scanning products after url preference toggled...\n") self.mac_prods = self.get_dict_for_prods(self.get_installers()) return @@ -541,7 +529,6 @@ class gibMacOS: def get_latest(self, device_id = None, dmg = False): self.u.head("Downloading Latest") - print("") prods = sorted(self.mac_prods, key=lambda x:x['version'], reverse=True) if device_id: prod = next(p for p in prods if device_id.lower() in p["device_ids"]) @@ -553,7 +540,6 @@ class gibMacOS: def get_for_product(self, prod, dmg = False): self.u.head("Downloading for {}".format(prod)) - print("") for p in self.mac_prods: if p["product"] == prod: self.download_prod(p, dmg) @@ -562,7 +548,6 @@ class gibMacOS: def get_for_version(self, vers, build = None, device_id = None, dmg = False): self.u.head("Downloading for {} {}".format(vers, build or "")) - print("") # Map the versions to their names v = self.version_names.get(vers.lower(),vers.lower()) v_dict = {} @@ -657,7 +642,6 @@ if __name__ == '__main__': g.main(args.dmg) except ProgramError as e: g.u.head(e.title) - print("") print(str(e)) print("") g.u.grab("Press [enter] to return...") From 36e51f43f7f6a959b28b3790d77557d5cff7b715 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 21:57:40 +0100 Subject: [PATCH 5/8] Log to stderr when running in non-interactive mode So we can use stdout for data, e.g. printing product metadata in JSON. --- Scripts/utils.py | 8 ++++++- gibMacOS.py | 55 +++++++++++++++++++++--------------------------- 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/Scripts/utils.py b/Scripts/utils.py index 1953c1b..e01bf1c 100755 --- a/Scripts/utils.py +++ b/Scripts/utils.py @@ -222,7 +222,7 @@ class Utils: # Header drawing method def head(self, text = None, width = 55): if not self.interactive: - print(text) + print(text, file=sys.stderr) return if text == None: text = self.name @@ -241,6 +241,12 @@ class Utils: print("#"*width) print("") + def info(self, text): + if self.interactive: + print(text) + else: + print(text, file=sys.stderr) + def resize(self, width, height): print('\033[8;{};{}t'.format(height, width)) diff --git a/gibMacOS.py b/gibMacOS.py index 5dd6bbe..51d0d6d 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -106,7 +106,7 @@ class gibMacOS: message += "Please ensure you have a working internet connection." raise ProgramError(message, title="Catalog Data Error") self.u.head("Parsing Data") - print("Scanning products after catalog download...\n") + self.u.info("Scanning products after catalog download...\n") self.mac_prods = self.get_dict_for_prods(self.get_installers()) def set_catalog(self, catalog): @@ -151,40 +151,40 @@ class gibMacOS: url = self.build_url(catalog=self.current_catalog, version=self.current_macos) self.u.head("Downloading Catalog") if local: - print("Checking locally for {}".format(self.plist)) + self.u.info("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...") + self.u.info(" - 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") + self.u.info(" - 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)) + self.u.info(" - Not found - downloading instead...\n") + self.u.info("Currently downloading {} catalog from:\n\n{}\n".format(self.current_catalog, url)) try: - b = self.d.get_bytes(url) - print("") + b = self.d.get_bytes(url, self.interactive) + self.u.info("") self.catalog_data = plist.loads(b) except: - print("Error downloading!") + self.u.info("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)) + self.u.info(" - 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!") + self.u.info(" - Error saving!") return False return True @@ -274,7 +274,7 @@ class gibMacOS: # Attempt to get the build/version/name/device-ids info from the dist prodd["build"],v,n,prodd["device_ids"] = self.get_build_version(plist_dict.get("Products",{}).get(prod,{}).get("Distributions",{})) prodd["title"] = smd.get("localization",{}).get("English",{}).get("title",n) - print(" -->{}. {} ({}){}".format( + self.u.info(" -->{}. {} ({}){}".format( str(len(prod_list)+1).rjust(3), prodd["title"], prodd["build"], @@ -317,8 +317,7 @@ class gibMacOS: if os.path.exists(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)): while True: self.u.head("Already Exists") - print("It looks like you've already downloaded {}".format(name)) - print("") + self.u.info("It looks like you've already downloaded {}\n".format(name)) if not self.interactive: return menu = self.u.grab("Redownload? (y/n): ") @@ -336,13 +335,11 @@ class gibMacOS: c += 1 self.u.head("Downloading File {} of {}".format(c, len(dl_list))) if len(done): - print("\n".join(["{} --> {}".format(y["name"], "Succeeded" if y["status"] else "Failed") for y in done])) - print("") + self.u.info("\n".join(["{} --> {}".format(y["name"], "Succeeded" if y["status"] else "Failed") for y in done])) + self.u.info("") if dmg: - print("NOTE: Only Downloading DMG Files") - print("") - print("Downloading {} for {}...".format(os.path.basename(x), name)) - print("") + self.u.info("NOTE: Only Downloading DMG Files\n") + self.u.info("Downloading {} for {}...\n".format(os.path.basename(x), name)) try: self.d.stream_to_file(x, os.path.join(os.getcwd(), self.saves, self.current_catalog, name, os.path.basename(x))) done.append({"name":os.path.basename(x), "status":True}) @@ -351,23 +348,19 @@ class gibMacOS: succeeded = [x for x in done if x["status"]] failed = [x for x in done if not x["status"]] self.u.head("Downloaded {} of {}".format(len(succeeded), len(dl_list))) - print("Succeeded:") + self.u.info("Succeeded:") if len(succeeded): for x in succeeded: - print(" {}".format(x["name"])) + self.u.info(" {}".format(x["name"])) else: - print(" None") - print("") - print("Failed:") + self.u.info(" None") + self.u.info("\nFailed:") if len(failed): for x in failed: - print(" {}".format(x["name"])) + self.u.info(" {}".format(x["name"])) else: - print(" None") - print("") - print("Files saved to:") - print(" {}".format(os.path.join(os.getcwd(), self.saves, self.current_catalog, name))) - print("") + self.u.info(" None") + self.u.info("\nFiles saved to:\n {}\n".format(os.path.join(os.getcwd(), self.saves, self.current_catalog, name))) if self.interactive: self.u.grab("Press [enter] to return...") elif len(failed): From 11bfad8304c9c02ba8e4c461f49450e43aa86cff Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 22:05:58 +0100 Subject: [PATCH 6/8] Add support for printing product metadata in JSON --- gibMacOS.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/gibMacOS.py b/gibMacOS.py index 51d0d6d..d33eb36 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -38,6 +38,7 @@ class gibMacOS: self.current_macos = self.settings.get("current_macos",17) # if > 16, assume X-5, else 10.X self.min_macos = 5 self.print_urls = self.settings.get("print_urls",False) + self.print_json = False self.mac_os_names_url = { "8" : "mountainlion", "7" : "lion", @@ -303,7 +304,10 @@ class gibMacOS: raise ProgramError("There were no files to download") c = 0 done = [] - if self.print_urls: + if self.print_json: + print(self.product_to_json(prod)) + return + elif self.print_urls: self.u.head("Download Links") print("{}:\n".format(name)) print("\n".join([" - {} \n --> {}".format(os.path.basename(x), x) for x in dl_list])) @@ -366,6 +370,14 @@ class gibMacOS: elif len(failed): raise ProgramError("{} files failed to download".format(len(failed))) + def product_to_json(self, prod): + return json.dumps({ + **{key: value for key, value in prod.items() + if key in ["product", "version", "build", "title", "size", "packages"]}, + "date": prod["date"].isoformat(), + "deviceIds": list(prod["device_ids"]), + }) + def show_catalog_url(self): self.resize() self.u.head() @@ -592,6 +604,7 @@ if __name__ == '__main__': parser.add_argument("-m", "--maxos", help="sets the max macOS version to consider when building the url - eg 10.14") parser.add_argument("-D", "--device-id", help="use with --version or --latest to search for versions supporting the specified Device ID - eg VMM-x86_64 for any x86_64") parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true") + parser.add_argument("-j", "--print-json", help="only prints the product metadata in JSON, does not actually download it", action="store_true") parser.add_argument("--no-interactive", help="run in non-interactive mode", action="store_true") args = parser.parse_args() @@ -609,6 +622,9 @@ if __name__ == '__main__': if args.print_urls: g.print_urls = True + if args.print_json: + g.print_json = True + if args.maxos: try: version = g.macos_to_num(args.maxos) From f2aa8234965def882da920822e1337e90cb51415 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 22:20:29 +0100 Subject: [PATCH 7/8] Fix shebang to python3 Python 2 is dead and the "python" command with it. macOS doesn't ship Python 2 anymore and doesn't provide "python" even as a symlink. --- gibMacOS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gibMacOS.py b/gibMacOS.py index d33eb36..2b7e9f1 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from Scripts import downloader,utils,run,plist import os, shutil, time, sys, argparse, re, json From 880d7f6319450ea94ae94bff15ffa1ef3b907c27 Mon Sep 17 00:00:00 2001 From: Jakub Jirutka Date: Tue, 21 Nov 2023 23:38:49 +0100 Subject: [PATCH 8/8] Allow to override download (output) directory using --download-dir --- gibMacOS.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/gibMacOS.py b/gibMacOS.py index 2b7e9f1..844651d 100755 --- a/gibMacOS.py +++ b/gibMacOS.py @@ -9,8 +9,9 @@ class ProgramError(Exception): class gibMacOS: - def __init__(self, interactive = True): + def __init__(self, interactive = True, download_dir = None): self.interactive = interactive + self.download_dir = download_dir self.d = downloader.Downloader() self.u = utils.Utils("gibMacOS", interactive=interactive) self.r = run.Run() @@ -65,7 +66,6 @@ class gibMacOS: self.catalog_data = None self.scripts = "Scripts" self.plist = "sucatalog.plist" - self.saves = "macOS Downloads" self.save_local = False self.force_local = False self.find_recovery = self.settings.get("find_recovery",False) @@ -292,6 +292,7 @@ class gibMacOS: # Takes a dictonary of details and downloads it self.resize() name = "{} - {} {} ({})".format(prod["product"], prod["version"], prod["title"], prod["build"]).replace(":","").strip() + download_dir = self.download_dir or os.path.join(os.path.dirname(os.path.realpath(__file__)), "macOS Downloads", self.current_catalog, name) dl_list = [] for x in prod["packages"]: if not x.get("URL",None): @@ -316,9 +317,7 @@ class gibMacOS: self.u.grab("Press [enter] to return...") return # Only check the dirs if we need to - cwd = os.getcwd() - os.chdir(os.path.dirname(os.path.realpath(__file__))) - if os.path.exists(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)): + if self.download_dir is None and os.path.exists(download_dir): while True: self.u.head("Already Exists") self.u.info("It looks like you've already downloaded {}\n".format(name)) @@ -332,9 +331,9 @@ class gibMacOS: if menu[0].lower() == "y": break # Remove the old copy, then re-download - shutil.rmtree(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)) + shutil.rmtree(download_dir) # Make it new - os.makedirs(os.path.join(os.getcwd(), self.saves, self.current_catalog, name)) + os.makedirs(download_dir) for x in dl_list: c += 1 self.u.head("Downloading File {} of {}".format(c, len(dl_list))) @@ -345,7 +344,7 @@ class gibMacOS: self.u.info("NOTE: Only Downloading DMG Files\n") self.u.info("Downloading {} for {}...\n".format(os.path.basename(x), name)) try: - self.d.stream_to_file(x, os.path.join(os.getcwd(), self.saves, self.current_catalog, name, os.path.basename(x))) + self.d.stream_to_file(x, os.path.join(download_dir, os.path.basename(x))) done.append({"name":os.path.basename(x), "status":True}) except: done.append({"name":os.path.basename(x), "status":False}) @@ -364,7 +363,7 @@ class gibMacOS: self.u.info(" {}".format(x["name"])) else: self.u.info(" None") - self.u.info("\nFiles saved to:\n {}\n".format(os.path.join(os.getcwd(), self.saves, self.current_catalog, name))) + self.u.info("\nFiles saved to:\n {}\n".format(download_dir)) if self.interactive: self.u.grab("Press [enter] to return...") elif len(failed): @@ -606,9 +605,10 @@ if __name__ == '__main__': parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true") parser.add_argument("-j", "--print-json", help="only prints the product metadata in JSON, does not actually download it", action="store_true") parser.add_argument("--no-interactive", help="run in non-interactive mode", action="store_true") + parser.add_argument("-o", "--download-dir", help="overrides directory where the downloaded files are saved") args = parser.parse_args() - g = gibMacOS(interactive=not args.no_interactive) + g = gibMacOS(interactive=not args.no_interactive, download_dir=args.download_dir) if args.recovery: args.dmg = False g.find_recovery = args.recovery