Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
corpnewt
GitHub Repository: corpnewt/gibMacOS
Path: blob/master/gibMacOS.py
174 views
1
#!/usr/bin/env python3
2
from Scripts import downloader,utils,run,plist
3
import os, shutil, time, sys, argparse, re, json, subprocess
4
5
class ProgramError(Exception):
6
def __init__(self, message, title = "Error"):
7
super(Exception, self).__init__(message)
8
self.title = title
9
10
11
class gibMacOS:
12
def __init__(self, interactive = True, download_dir = None):
13
self.interactive = interactive
14
self.download_dir = download_dir
15
self.d = downloader.Downloader()
16
self.u = utils.Utils("gibMacOS", interactive=interactive)
17
self.r = run.Run()
18
self.min_w = 80
19
self.min_h = 24
20
if os.name == "nt":
21
self.min_w = 120
22
self.min_h = 30
23
self.resize()
24
25
self.catalog_suffix = {
26
"public" : "beta",
27
"publicrelease" : "",
28
"customer" : "customerseed",
29
"developer" : "seed"
30
}
31
32
# Load settings.json if it exists in the Scripts folder
33
self.settings_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"Scripts","settings.json")
34
self.settings = {}
35
if os.path.exists(self.settings_path):
36
try: self.settings = json.load(open(self.settings_path))
37
except: pass
38
39
# Load prod_cache.json if it exists in the Scripts folder
40
self.prod_cache_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"Scripts","prod_cache.plist")
41
self.prod_cache = {}
42
if os.path.exists(self.prod_cache_path):
43
try:
44
with open(self.prod_cache_path,"rb") as f:
45
self.prod_cache = plist.load(f)
46
assert isinstance(self.prod_cache,dict)
47
except:
48
self.prod_cache = {}
49
50
# If > 16, assume X-5, else 10.X
51
# e.g. 17 = Monterey, 18 = Ventura, 19 = Sonoma, 20 = Sequoia
52
self.current_macos = self.settings.get("current_macos",20)
53
self.min_macos = 5
54
self.print_urls = self.settings.get("print_urls",False)
55
self.print_json = False
56
self.hide_pid = self.settings.get("hide_pid",False)
57
self.mac_os_names_url = {
58
"8" : "mountainlion",
59
"7" : "lion",
60
"6" : "snowleopard",
61
"5" : "leopard"
62
}
63
self.version_names = {
64
"tiger" : "10.4",
65
"leopard" : "10.5",
66
"snow leopard" : "10.6",
67
"lion" : "10.7",
68
"mountain lion" : "10.8",
69
"mavericks" : "10.9",
70
"yosemite" : "10.10",
71
"el capitan" : "10.11",
72
"sierra" : "10.12",
73
"high sierra" : "10.13",
74
"mojave" : "10.14",
75
"catalina" : "10.15",
76
"big sur" : "11",
77
"monterey" : "12",
78
"ventura" : "13",
79
"sonoma" : "14",
80
"sequoia" : "15",
81
"tahoe" : "26"
82
}
83
self.current_catalog = self.settings.get("current_catalog","publicrelease")
84
self.catalog_data = None
85
self.scripts = "Scripts"
86
self.local_catalog = os.path.join(os.path.dirname(os.path.realpath(__file__)),self.scripts,"sucatalog.plist")
87
self.caffeinate_downloads = self.settings.get("caffeinate_downloads",True)
88
self.caffeinate_process = None
89
self.save_local = False
90
self.force_local = False
91
self.find_recovery = self.settings.get("find_recovery",False)
92
self.recovery_suffixes = (
93
"RecoveryHDUpdate.pkg",
94
"RecoveryHDMetaDmg.pkg"
95
)
96
self.settings_to_save = (
97
"current_macos",
98
"current_catalog",
99
"print_urls",
100
"find_recovery",
101
"hide_pid",
102
"caffeinate_downloads"
103
)
104
self.mac_prods = []
105
106
def resize(self, width=0, height=0):
107
if not self.interactive:
108
return
109
width = width if width > self.min_w else self.min_w
110
height = height if height > self.min_h else self.min_h
111
self.u.resize(width, height)
112
113
def save_settings(self):
114
# Ensure we're using the latest values
115
for setting in self.settings_to_save:
116
self.settings[setting] = getattr(self,setting,None)
117
try:
118
json.dump(self.settings,open(self.settings_path,"w"),indent=2)
119
except Exception as e:
120
raise ProgramError(
121
"Failed to save settings to:\n\n{}\n\nWith error:\n\n - {}\n".format(self.settings_path,repr(e)),
122
title="Error Saving Settings")
123
124
def save_prod_cache(self):
125
try:
126
with open(self.prod_cache_path,"wb") as f:
127
plist.dump(self.prod_cache,f)
128
except Exception as e:
129
raise ProgramError(
130
"Failed to save product cache to:\n\n{}\n\nWith error:\n\n - {}\n".format(self.prod_cache_path,repr(e)),
131
title="Error Saving Product Cache")
132
133
def set_prods(self):
134
self.resize()
135
if not self.get_catalog_data(self.save_local):
136
message = "The currently selected catalog ({}) was not reachable\n".format(self.current_catalog)
137
if self.save_local:
138
message += "and I was unable to locate a valid catalog file at:\n - {}\n".format(
139
self.local_catalog
140
)
141
message += "Please ensure you have a working internet connection."
142
if self.interactive:
143
print(message)
144
self.u.grab("\nPress [enter] to continue...")
145
return
146
raise ProgramError(message, title="Catalog Data Error")
147
self.u.head("Parsing Data")
148
self.u.info("Scanning products after catalog download...\n")
149
self.mac_prods = self.get_dict_for_prods(self.get_installers())
150
151
def set_catalog(self, catalog):
152
self.current_catalog = catalog.lower() if catalog.lower() in self.catalog_suffix else "publicrelease"
153
154
def num_to_macos(self,macos_num,for_url=True):
155
if for_url: # Resolve 8-5 to their names and show Big Sur as 10.16
156
return self.mac_os_names_url.get(str(macos_num),"10.{}".format(macos_num)) if macos_num <= 16 else str(macos_num-5)
157
# Return 10.xx for anything Catalina and lower, otherwise 11+
158
return "10.{}".format(macos_num) if macos_num <= 15 else str(macos_num-5)
159
160
def macos_to_num(self,macos):
161
try:
162
macos_parts = [int(x) for x in macos.split(".")][:2 if macos.startswith("10.") else 1]
163
if macos_parts[0] == 11: macos_parts = [10,16] # Big sur
164
except:
165
return None
166
if len(macos_parts) > 1: return macos_parts[1]
167
return 5+macos_parts[0]
168
169
def get_macos_versions(self,minos=None,maxos=None,catalog=""):
170
if minos is None: minos = self.min_macos
171
if maxos is None: maxos = self.current_macos
172
if minos > maxos: minos,maxos = maxos,minos # Ensure min is less than or equal
173
os_versions = [self.num_to_macos(x,for_url=True) for x in range(minos,min(maxos+1,21))] # until sequoia
174
if maxos > 30: # since tahoe
175
os_versions.extend([self.num_to_macos(x,for_url=True) for x in range(31,maxos+1)])
176
if catalog:
177
# We have a custom catalog - prepend the first entry + catalog to the list
178
custom_cat_entry = os_versions[-1]+catalog
179
os_versions.append(custom_cat_entry)
180
return os_versions
181
182
def build_url(self, **kwargs):
183
catalog = kwargs.get("catalog", self.current_catalog).lower()
184
catalog = catalog if catalog.lower() in self.catalog_suffix else "publicrelease"
185
version = int(kwargs.get("version", self.current_macos))
186
return "https://swscan.apple.com/content/catalogs/others/index-{}.merged-1.sucatalog".format(
187
"-".join(reversed(self.get_macos_versions(self.min_macos,version,catalog=self.catalog_suffix.get(catalog,""))))
188
)
189
190
def get_catalog_data(self, local = False):
191
# Gets the data based on our current_catalog
192
url = self.build_url(catalog=self.current_catalog, version=self.current_macos)
193
self.u.head("Downloading Catalog")
194
if local:
195
self.u.info("Checking for:\n - {}".format(
196
self.local_catalog
197
))
198
if os.path.exists(self.local_catalog):
199
self.u.info(" - Found - loading...")
200
try:
201
with open(self.local_catalog, "rb") as f:
202
self.catalog_data = plist.load(f)
203
assert isinstance(self.catalog_data,dict)
204
return True
205
except Exception as e:
206
self.u.info(" - Error loading: {}".format(e))
207
self.u.info(" - Downloading instead...\n")
208
else:
209
self.u.info(" - Not found - downloading instead...\n")
210
self.u.info("Currently downloading {} catalog from:\n\n{}\n".format(self.current_catalog, url))
211
try:
212
b = self.d.get_bytes(url, self.interactive)
213
self.u.info("")
214
self.catalog_data = plist.loads(b)
215
except:
216
self.u.info("Error downloading!")
217
return False
218
try:
219
# Assume it's valid data - dump it to a local file
220
if local or self.force_local:
221
self.u.info(" - Saving to:\n - {}".format(
222
self.local_catalog
223
))
224
with open(self.local_catalog, "wb") as f:
225
plist.dump(self.catalog_data, f)
226
except Exception as e:
227
self.u.info(" - Error saving: {}".format(e))
228
return False
229
return True
230
231
def get_installers(self, plist_dict = None):
232
if not plist_dict:
233
plist_dict = self.catalog_data
234
if not plist_dict:
235
return []
236
mac_prods = []
237
for p in plist_dict.get("Products", {}):
238
if not self.find_recovery:
239
val = plist_dict.get("Products",{}).get(p,{}).get("ExtendedMetaInfo",{}).get("InstallAssistantPackageIdentifiers",{})
240
if val.get("OSInstall",{}) == "com.apple.mpkg.OSInstall" or val.get("SharedSupport","").startswith("com.apple.pkg.InstallAssistant"):
241
mac_prods.append(p)
242
else:
243
# Find out if we have any of the recovery_suffixes
244
if any(x for x in plist_dict.get("Products",{}).get(p,{}).get("Packages",[]) if x["URL"].endswith(self.recovery_suffixes)):
245
mac_prods.append(p)
246
return mac_prods
247
248
def get_build_version(self, dist_dict):
249
build = version = name = "Unknown"
250
try:
251
dist_url = dist_dict.get("English",dist_dict.get("en",""))
252
dist_file = self.d.get_string(dist_url,False)
253
assert isinstance(dist_file,str)
254
except:
255
dist_file = ""
256
build_search = "macOSProductBuildVersion" if "macOSProductBuildVersion" in dist_file else "BUILD"
257
vers_search = "macOSProductVersion" if "macOSProductVersion" in dist_file else "VERSION"
258
try:
259
build = dist_file.split("<key>{}</key>".format(build_search))[1].split("<string>")[1].split("</string>")[0]
260
except:
261
pass
262
try:
263
version = dist_file.split("<key>{}</key>".format(vers_search))[1].split("<string>")[1].split("</string>")[0]
264
except:
265
pass
266
try:
267
name = re.search(r"<title>(.+?)</title>",dist_file).group(1)
268
except:
269
pass
270
try:
271
# XXX: This is parsing a JavaScript array from the script part of the dist file.
272
device_ids = re.search(r"var supportedDeviceIDs\s*=\s*\[([^]]+)\];", dist_file)[1]
273
device_ids = list(set(i.lower() for i in re.findall(r"'([^',]+)'", device_ids)))
274
except:
275
device_ids = []
276
return (build,version,name,device_ids)
277
278
def get_dict_for_prods(self, prods, plist_dict = None):
279
plist_dict = plist_dict or self.catalog_data or {}
280
prod_list = []
281
# Keys required to consider a cached element valid
282
prod_keys = (
283
"build",
284
"date",
285
"description",
286
"device_ids",
287
"installer",
288
"product",
289
"time",
290
"title",
291
"version",
292
)
293
294
def get_packages_and_size(plist_dict,prod,recovery):
295
# Iterate the available packages and save their urls and sizes
296
packages = []
297
size = -1
298
if recovery:
299
# Only get the recovery packages
300
packages = [x for x in plist_dict.get("Products",{}).get(prod,{}).get("Packages",[]) if x["URL"].endswith(self.recovery_suffixes)]
301
else:
302
# Add them all!
303
packages = plist_dict.get("Products",{}).get(prod,{}).get("Packages",[])
304
# Get size
305
size = self.d.get_size(sum([i["Size"] for i in packages]))
306
return (packages,size)
307
308
def print_prod(prod,prod_list):
309
self.u.info(" -->{}. {} ({}){}".format(
310
str(len(prod_list)+1).rjust(3),
311
prod["title"],
312
prod["build"],
313
" - FULL Install" if self.find_recovery and prod["installer"] else ""
314
))
315
316
def prod_valid(prod,prod_list,prod_keys):
317
# Check if the prod has all prod keys, and
318
# none are "Unknown"
319
if not isinstance(prod_list,dict) or not prod in prod_list or \
320
not all(x in prod_list[prod] for x in prod_keys):
321
# Wrong type, missing the prod, or prod_list keys
322
return False
323
# Let's make sure none of the keys return Unknown
324
if any(prod_list[prod].get(x,"Unknown")=="Unknown" for x in prod_keys):
325
return False
326
return True
327
328
# Boolean to keep track of cache updates
329
prod_changed = False
330
for prod in prods:
331
if prod_valid(prod,self.prod_cache,prod_keys):
332
# Already have it - and it's valid.
333
# Create a shallow copy
334
prodd = {}
335
for key in self.prod_cache[prod]:
336
prodd[key] = self.prod_cache[prod][key]
337
# Update the packages and size lists
338
prodd["packages"],prodd["size"] = get_packages_and_size(plist_dict,prod,self.find_recovery)
339
# Add to our list and continue on
340
prod_list.append(prodd)
341
# Log the product
342
print_prod(prodd,prod_list)
343
continue
344
# Grab the ServerMetadataURL for the passed product key if it exists
345
prodd = {"product":prod}
346
try:
347
url = plist_dict.get("Products",{}).get(prod,{}).get("ServerMetadataURL","")
348
assert url
349
b = self.d.get_bytes(url,False)
350
smd = plist.loads(b)
351
except:
352
smd = {}
353
# Populate some info!
354
prodd["date"] = plist_dict.get("Products",{}).get(prod,{}).get("PostDate","")
355
prodd["installer"] = plist_dict.get("Products",{}).get(prod,{}).get("ExtendedMetaInfo",{}).get("InstallAssistantPackageIdentifiers",{}).get("OSInstall",{}) == "com.apple.mpkg.OSInstall"
356
prodd["time"] = time.mktime(prodd["date"].timetuple()) + prodd["date"].microsecond / 1E6
357
prodd["version"] = smd.get("CFBundleShortVersionString","Unknown").strip()
358
# Try to get the description too
359
try:
360
desc = smd.get("localization",{}).get("English",{}).get("description","").decode("utf-8")
361
desctext = desc.split('"p1">')[1].split("</a>")[0]
362
except:
363
desctext = ""
364
prodd["description"] = desctext
365
prodd["packages"],prodd["size"] = get_packages_and_size(plist_dict,prod,self.find_recovery)
366
# Get size
367
prodd["size"] = self.d.get_size(sum([i["Size"] for i in prodd["packages"]]))
368
# Attempt to get the build/version/name/device-ids info from the dist
369
prodd["build"],v,n,prodd["device_ids"] = self.get_build_version(plist_dict.get("Products",{}).get(prod,{}).get("Distributions",{}))
370
prodd["title"] = smd.get("localization",{}).get("English",{}).get("title",n)
371
if v.lower() != "unknown":
372
prodd["version"] = v
373
prod_list.append(prodd)
374
# If we were able to resolve the SMD URL - or it didn't exist, save it to the cache
375
if smd or not plist_dict.get("Products",{}).get(prod,{}).get("ServerMetadataURL",""):
376
prod_changed = True
377
# Create a temp prod dict so we can save all but the packages and
378
# size keys - as those are determined based on self.find_recovery
379
temp_prod = {}
380
for key in prodd:
381
if key in ("packages","size"): continue
382
if prodd[key] == "Unknown":
383
# Don't cache Unknown values
384
temp_prod = None
385
break
386
temp_prod[key] = prodd[key]
387
if temp_prod:
388
# Only update the cache if it changed
389
self.prod_cache[prod] = temp_prod
390
# Log the product
391
print_prod(prodd,prod_list)
392
# Try saving the cache for later
393
if prod_changed and self.prod_cache:
394
try: self.save_prod_cache()
395
except: pass
396
# Sort by newest
397
prod_list = sorted(prod_list, key=lambda x:x["time"], reverse=True)
398
return prod_list
399
400
def start_caffeinate(self):
401
# Check if we need to caffeinate
402
if sys.platform.lower() == "darwin" \
403
and self.caffeinate_downloads \
404
and os.path.isfile("/usr/bin/caffeinate"):
405
# Terminate any existing caffeinate process
406
self.term_caffeinate_proc()
407
# Create a new caffeinate process
408
self.caffeinate_process = subprocess.Popen(
409
["/usr/bin/caffeinate"],
410
stderr=getattr(subprocess,"DEVNULL",open(os.devnull,"w")),
411
stdout=getattr(subprocess,"DEVNULL",open(os.devnull,"w")),
412
stdin=getattr(subprocess,"DEVNULL",open(os.devnull,"w"))
413
)
414
return self.caffeinate_process
415
416
def term_caffeinate_proc(self):
417
if self.caffeinate_process is None:
418
return True
419
try:
420
if self.caffeinate_process.poll() is None:
421
# Save the time we started waiting
422
start = time.time()
423
while self.caffeinate_process.poll() is None:
424
# Make sure we haven't waited too long
425
if time.time() - start > 10:
426
print(" - Timed out trying to terminate caffeinate process with PID {}!".format(
427
self.caffeinate_process.pid
428
))
429
return False
430
# It's alive - terminate it
431
self.caffeinate_process.terminate()
432
# Sleep to let things settle
433
time.sleep(0.02)
434
except:
435
pass
436
return True # Couldn't poll - or we termed it
437
438
def download_prod(self, prod, dmg = False):
439
# Takes a dictonary of details and downloads it
440
self.resize()
441
name = "{} - {} {} ({})".format(prod["product"], prod["version"], prod["title"], prod["build"]).replace(":","").strip()
442
download_dir = self.download_dir or os.path.join(os.path.dirname(os.path.realpath(__file__)), "macOS Downloads", self.current_catalog, name)
443
dl_list = []
444
for x in prod["packages"]:
445
if not x.get("URL",None):
446
continue
447
if dmg and not x.get("URL","").lower().endswith(".dmg"):
448
continue
449
# add it to the list
450
dl_list.append(x)
451
if not len(dl_list):
452
raise ProgramError("There were no files to download")
453
done = []
454
if self.print_json:
455
print(self.product_to_json(prod))
456
if self.interactive:
457
print("")
458
self.u.grab("Press [enter] to return...")
459
return
460
elif self.print_urls:
461
self.u.head("Download Links")
462
print("{}:\n".format(name))
463
print("\n".join([" - {} ({}) \n --> {}".format(
464
os.path.basename(x["URL"]),
465
self.d.get_size(x["Size"],strip_zeroes=True) if x.get("Size") is not None else "?? MB",
466
x["URL"]
467
) for x in dl_list]))
468
if self.interactive:
469
print("")
470
self.u.grab("Press [enter] to return...")
471
return
472
# Only check the dirs if we need to
473
if self.download_dir is None and os.path.exists(download_dir):
474
while True:
475
self.u.head("Already Exists")
476
self.u.info("It looks like you've already downloaded the following package:\n{}\n".format(name))
477
if not self.interactive:
478
menu = "r"
479
else:
480
print("R. Resume Incomplete Files")
481
print("D. Redownload All Files")
482
print("")
483
print("M. Return")
484
print("Q. Quit")
485
print("")
486
menu = self.u.grab("Please select an option: ")
487
if not len(menu):
488
continue
489
elif menu.lower() == "q":
490
self.u.custom_quit()
491
elif menu.lower() == "m":
492
return
493
elif menu.lower() == "r":
494
break
495
elif menu.lower() == "d":
496
# Remove the old copy, then re-download
497
shutil.rmtree(download_dir)
498
break
499
# Make it anew as needed
500
if not os.path.isdir(download_dir):
501
os.makedirs(download_dir)
502
# Clean up any leftover or missed caffeinate
503
# procs
504
self.term_caffeinate_proc()
505
for c,x in enumerate(dl_list,start=1):
506
url = x["URL"]
507
self.u.head("Downloading File {} of {}".format(c, len(dl_list)))
508
self.u.info("- {} -\n".format(name))
509
if len(done):
510
self.u.info("\n".join(["{} --> {}".format(y["name"], "Succeeded" if y["status"] else "Failed") for y in done]))
511
self.u.info("")
512
if dmg:
513
self.u.info("NOTE: Only Downloading DMG Files\n")
514
self.u.info("Downloading {}...\n".format(os.path.basename(url)))
515
try:
516
# Caffeinate as needed
517
self.start_caffeinate()
518
result = self.d.stream_to_file(url, os.path.join(download_dir, os.path.basename(url)), allow_resume=True)
519
assert result is not None
520
done.append({"name":os.path.basename(url), "status":True})
521
except:
522
done.append({"name":os.path.basename(url), "status":False})
523
# Kill caffeinate if we need to
524
self.term_caffeinate_proc()
525
succeeded = [x for x in done if x["status"]]
526
failed = [x for x in done if not x["status"]]
527
self.u.head("Downloaded {} of {}".format(len(succeeded), len(dl_list)))
528
self.u.info("- {} -\n".format(name))
529
self.u.info("Succeeded:")
530
if len(succeeded):
531
for x in succeeded:
532
self.u.info(" {}".format(x["name"]))
533
else:
534
self.u.info(" None")
535
self.u.info("\nFailed:")
536
if len(failed):
537
for x in failed:
538
self.u.info(" {}".format(x["name"]))
539
else:
540
self.u.info(" None")
541
self.u.info("\nFiles saved to:\n {}\n".format(download_dir))
542
if self.interactive:
543
self.u.grab("Press [enter] to return...")
544
elif len(failed):
545
raise ProgramError("{} files failed to download".format(len(failed)))
546
547
def product_to_json(self, prod):
548
prod_dict = {}
549
for key in ["product", "version", "build", "title", "size", "packages"]:
550
if key in prod:
551
prod_dict[key] = prod[key]
552
prod_dict["date"] = prod["date"].isoformat()
553
prod_dict["deviceIds"] = list(prod["device_ids"])
554
return json.dumps(prod_dict,indent=2)
555
556
def show_catalog_url(self):
557
self.resize()
558
self.u.head()
559
print("Current Catalog: {}".format(self.current_catalog))
560
print("Max macOS Version: {}".format(self.num_to_macos(self.current_macos,for_url=False)))
561
print("")
562
print("{}".format(self.build_url()))
563
if self.interactive:
564
print("")
565
self.u.grab("Press [enter] to return...")
566
567
def pick_catalog(self):
568
self.resize()
569
self.u.head("Select SU Catalog")
570
count = 0
571
for x in self.catalog_suffix:
572
count += 1
573
print("{}. {}".format(count, x))
574
print("")
575
print("M. Main Menu")
576
print("Q. Quit")
577
print("")
578
menu = self.u.grab("Please select an option: ")
579
if not len(menu):
580
self.pick_catalog()
581
return
582
if menu[0].lower() == "m":
583
return
584
elif menu[0].lower() == "q":
585
self.u.custom_quit()
586
# Should have something to test here
587
try:
588
i = int(menu)
589
self.current_catalog = list(self.catalog_suffix)[i-1]
590
self.save_settings()
591
except:
592
# Incorrect - try again
593
self.pick_catalog()
594
return
595
# If we made it here - then we got something
596
# Reload with the proper catalog
597
self.get_catalog_data()
598
599
def pick_macos(self):
600
self.resize()
601
self.u.head("Select Max macOS Version")
602
print("Currently set to {}".format(self.num_to_macos(self.current_macos,for_url=False)))
603
print("")
604
print("M. Main Menu")
605
print("Q. Quit")
606
print("")
607
print("Please type the max macOS version for the catalog url")
608
menu = self.u.grab("eg. 10.15 for Catalina, 11 for Big Sur, 12 for Monterey: ")
609
if not len(menu):
610
self.pick_macos()
611
return
612
if menu[0].lower() == "m":
613
return
614
elif menu[0].lower() == "q":
615
self.u.custom_quit()
616
# At this point - we should have something in the proper format
617
version = self.macos_to_num(menu)
618
if not version: return
619
self.current_macos = version
620
self.save_settings()
621
# At this point, we should be good - set teh catalog
622
# data - but if it fails, remove the listed prods
623
if not self.get_catalog_data():
624
self.catalog_data = None
625
self.u.grab("\nPress [enter] to return...")
626
self.pick_macos()
627
return
628
629
def main(self, dmg = False):
630
lines = []
631
lines.append("Available Products:")
632
lines.append(" ")
633
if not len(self.mac_prods):
634
lines.append("No installers in catalog!")
635
lines.append(" ")
636
for num,p in enumerate(self.mac_prods,start=1):
637
var1 = "{}. {} {}".format(str(num).rjust(2), p["title"], p["version"])
638
var2 = ""
639
if p["build"].lower() != "unknown":
640
var1 += " ({})".format(p["build"])
641
if not self.hide_pid:
642
var2 = " - {} - Added {} - {}".format(p["product"], p["date"], p["size"])
643
if self.find_recovery and p["installer"]:
644
# Show that it's a full installer
645
if self.hide_pid:
646
var1 += " - FULL Install"
647
else:
648
var2 += " - FULL Install"
649
lines.append(var1)
650
if not self.hide_pid:
651
lines.append(var2)
652
lines.append(" ")
653
lines.append("M. Change Max-OS Version (Currently {})".format(self.num_to_macos(self.current_macos,for_url=False)))
654
lines.append("C. Change Catalog (Currently {})".format(self.current_catalog))
655
lines.append("I. Only Print URLs (Currently {})".format("On" if self.print_urls else "Off"))
656
lines.append("H. {} Package IDs and Upload Dates".format("Show" if self.hide_pid else "Hide"))
657
if sys.platform.lower() == "darwin":
658
lines.append("S. Set Current Catalog to SoftwareUpdate Catalog")
659
lines.append("L. Clear SoftwareUpdate Catalog")
660
lines.append("F. Caffeinate Downloads to Prevent Sleep (Currently {})".format("On" if self.caffeinate_downloads else "Off"))
661
lines.append("R. Toggle Recovery-Only (Currently {})".format("On" if self.find_recovery else "Off"))
662
lines.append("U. Show Catalog URL")
663
lines.append("Q. Quit")
664
lines.append(" ")
665
self.resize(len(max(lines)), len(lines)+5)
666
self.u.head()
667
print("\n".join(lines))
668
menu = self.u.grab("Please select an option: ")
669
if not len(menu):
670
return
671
if menu[0].lower() == "q":
672
self.resize()
673
self.u.custom_quit()
674
elif menu[0].lower() == "u":
675
self.show_catalog_url()
676
elif menu[0].lower() == "m":
677
self.pick_macos()
678
elif menu[0].lower() == "c":
679
self.pick_catalog()
680
elif menu[0].lower() == "i":
681
self.print_urls ^= True
682
self.save_settings()
683
elif menu[0].lower() == "h":
684
self.hide_pid ^= True
685
self.save_settings()
686
elif menu[0].lower() == "s" and sys.platform.lower() == "darwin":
687
# Set the software update catalog to our current catalog url
688
self.u.head("Setting SU CatalogURL")
689
url = self.build_url(catalog=self.current_catalog, version=self.current_macos)
690
print("Setting catalog URL to:\n{}".format(url))
691
print("")
692
print("sudo softwareupdate --set-catalog {}".format(url))
693
self.r.run({"args":["softwareupdate","--set-catalog",url],"sudo":True})
694
print("")
695
self.u.grab("Done",timeout=5)
696
elif menu[0].lower() == "l" and sys.platform.lower() == "darwin":
697
# Clear the software update catalog
698
self.u.head("Clearing SU CatalogURL")
699
print("sudo softwareupdate --clear-catalog")
700
self.r.run({"args":["softwareupdate","--clear-catalog"],"sudo":True})
701
print("")
702
self.u.grab("Done.", timeout=5)
703
elif menu[0].lower() == "f" and sys.platform.lower() == "darwin":
704
# Toggle our caffeinate downloads value and save settings
705
self.caffeinate_downloads ^= True
706
self.save_settings()
707
elif menu[0].lower() == "r":
708
self.find_recovery ^= True
709
self.save_settings()
710
if menu[0].lower() in ["m","c","r"]:
711
self.resize()
712
self.u.head("Parsing Data")
713
print("Re-scanning products after url preference toggled...\n")
714
self.mac_prods = self.get_dict_for_prods(self.get_installers())
715
else:
716
# Assume we picked something
717
try:
718
menu = int(menu)
719
except:
720
return
721
if menu < 1 or menu > len(self.mac_prods):
722
return
723
self.download_prod(self.mac_prods[menu-1], dmg)
724
725
def get_latest(self, device_id = None, dmg = False):
726
self.u.head("Downloading Latest")
727
prods = sorted(self.mac_prods, key=lambda x:x['version'], reverse=True)
728
if device_id:
729
prod = next(p for p in prods if device_id.lower() in p["device_ids"])
730
if not prod:
731
raise ProgramError("No version found for Device ID '{}'".format(device_id))
732
else:
733
prod = prods[0]
734
self.download_prod(prod, dmg)
735
736
def get_for_product(self, prod, dmg = False):
737
self.u.head("Downloading for {}".format(prod))
738
for p in self.mac_prods:
739
if p["product"] == prod:
740
self.download_prod(p, dmg)
741
return
742
raise ProgramError("{} not found".format(prod))
743
744
def get_for_version(self, vers, build = None, device_id = None, dmg = False):
745
self.u.head("Downloading for {} {}".format(vers, build or ""))
746
# Map the versions to their names
747
v = self.version_names.get(vers.lower(),vers.lower())
748
v_dict = {}
749
for n in self.version_names:
750
v_dict[self.version_names[n]] = n
751
n = v_dict.get(v, v)
752
for p in sorted(self.mac_prods, key=lambda x:x['version'], reverse=True):
753
if build and p["build"] != build:
754
continue
755
if device_id and device_id.lower() not in p["device_ids"]:
756
continue
757
pt = p["title"].lower()
758
pv = p["version"].lower()
759
# Need to compare verisons - n = name, v = version
760
# p["version"] and p["title"] may contain either the version
761
# or name - so check both
762
# We want to make sure, if we match the name to the title, that we only match
763
# once - so Sierra/High Sierra don't cross-match
764
#
765
# First check if p["version"] isn't " " or "1.0"
766
if not pv in [" ","1.0"]:
767
# Have a real version - match this first
768
if pv.startswith(v):
769
self.download_prod(p, dmg)
770
return
771
# Didn't match the version - or version was bad, let's check
772
# the title
773
# Need to make sure n is in the version name, but not equal to it,
774
# and the version name is in p["title"] to disqualify
775
# i.e. - "Sierra" exists in "High Sierra", but does not equal "High Sierra"
776
# and "High Sierra" is in "macOS High Sierra 10.13.6" - This would match
777
name_match = [x for x in self.version_names if n in x and x != n and x in pt]
778
if (n in pt) and not len(name_match):
779
self.download_prod(p, dmg)
780
return
781
raise ProgramError("'{}' '{}' not found".format(vers, build or ""))
782
783
if __name__ == '__main__':
784
parser = argparse.ArgumentParser()
785
parser.add_argument("-l", "--latest", help="downloads the version available in the current catalog (overrides --build, --version and --product)", action="store_true")
786
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")
787
parser.add_argument("-d", "--dmg", help="downloads only the .dmg files", action="store_true")
788
parser.add_argument("-s", "--savelocal", help="uses a locally saved sucatalog.plist if exists", action="store_true")
789
parser.add_argument("-g", "--local-catalog", help="the path to the sucatalog.plist to use (implies --savelocal)")
790
parser.add_argument("-n", "--newlocal", help="downloads and saves locally, overwriting any prior sucatalog.plist (will use the path from --local-catalog if provided)", action="store_true")
791
parser.add_argument("-c", "--catalog", help="sets the CATALOG to use - publicrelease, public, customer, developer")
792
parser.add_argument("-p", "--product", help="sets the product id to search for (overrides --version)")
793
parser.add_argument("-v", "--version", help="sets the version of macOS to target - eg '-v 10.14' or '-v Yosemite'")
794
parser.add_argument("-b", "--build", help="sets the build of macOS to target - eg '22G120' (must be used together with --version)")
795
parser.add_argument("-m", "--maxos", help="sets the max macOS version to consider when building the url - eg 10.14")
796
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")
797
parser.add_argument("-i", "--print-urls", help="only prints the download URLs, does not actually download them", action="store_true")
798
parser.add_argument("-j", "--print-json", help="only prints the product metadata in JSON, does not actually download it", action="store_true")
799
parser.add_argument("--no-interactive", help="run in non-interactive mode (auto-enabled when using --product or --version)", action="store_true")
800
parser.add_argument("-o", "--download-dir", help="overrides directory where the downloaded files are saved")
801
args = parser.parse_args()
802
803
if args.build and not (args.latest or args.product or args.version):
804
print("The --build option requires a --version")
805
exit(1)
806
807
interactive = not any((args.no_interactive,args.product,args.version))
808
g = gibMacOS(interactive=interactive, download_dir=args.download_dir)
809
810
if args.recovery:
811
args.dmg = False
812
g.find_recovery = args.recovery
813
814
if args.savelocal:
815
g.save_local = True
816
817
if args.local_catalog:
818
g.save_local = True
819
g.local_catalog = args.local_catalog
820
821
if args.newlocal:
822
g.force_local = True
823
824
if args.print_urls:
825
g.print_urls = True
826
827
if args.print_json:
828
g.print_json = True
829
830
if args.maxos:
831
try:
832
version = g.macos_to_num(args.maxos)
833
if version: g.current_macos = version
834
except:
835
pass
836
if args.catalog:
837
# Set the catalog
838
g.set_catalog(args.catalog)
839
840
try:
841
# Done setting up pre-requisites
842
g.set_prods()
843
844
if args.latest:
845
g.get_latest(device_id=args.device_id, dmg=args.dmg)
846
elif args.product != None:
847
g.get_for_product(args.product, args.dmg)
848
elif args.version != None:
849
g.get_for_version(args.version, args.build, device_id=args.device_id, dmg=args.dmg)
850
elif g.interactive:
851
while True:
852
try:
853
g.main(args.dmg)
854
except ProgramError as e:
855
g.u.head(e.title)
856
print(str(e))
857
print("")
858
g.u.grab("Press [enter] to return...")
859
else:
860
raise ProgramError("No command specified")
861
except ProgramError as e:
862
print(str(e))
863
if g.interactive:
864
print("")
865
g.u.grab("Press [enter] to exit...")
866
else:
867
exit(1)
868
exit(0)
869
870