diff --git a/README.md b/README.md index 5c80bff..1fef32e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,11 @@ This command converts the output of Imagr's `make nbi` into a bootable external This script can create disk images containing macOS Installer applications available via Apple's softwareupdate catalogs. -It does this by downloading the packages from Apple's softwareupdate servers and then installing them into a new empty disk image. +Run `./installinstallmacos.py --help` to see the available options. + +The tool assembles "Install macOS" applications by downloading the packages from Apple's softwareupdate servers and then installing them into a new empty disk image. + +If `/usr/bin/installer` returns errors during this process, it can be useful to examine `/var/log/install.log` for clues. Since it is using Apple's installer, any install check or volume check scripts are run. This means that you can only use this tool to create a diskimage containing the versions of macOS that will run on the exact machine you are running the script on. @@ -37,9 +41,7 @@ Command '['/usr/sbin/installer', '-pkg', './content/downloads/07/20/091-95774/aw Product installation failed. ``` -Use a compatible Mac or select a different build compatible with your current hardware and try again. - -Run `./installinstallmacos.py --help` to see the available options. +Use a compatible Mac or select a different build compatible with your current hardware and try again. You may also have success running the script in a VM; the InstallationCheck script in versions of the macOS installer to date skips the checks (and returns success) when run on a VM. #### make_firmwareupdater_pkg.sh diff --git a/installinstallmacos.py b/installinstallmacos.py index 35ec9f9..6264169 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -31,7 +31,6 @@ import plistlib import subprocess import re import sys -import time import urlparse import xattr from xml.dom import minidom @@ -40,10 +39,15 @@ from operator import itemgetter from distutils.version import LooseVersion -DEFAULT_SUCATALOG = ( - 'https://swscan.apple.com/content/catalogs/others/' - 'index-10.14seed-10.14-10.13-10.12-10.11-10.10-10.9' - '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog.gz') +DEFAULT_SUCATALOGS = { + '17': 'https://swscan.apple.com/content/catalogs/others/' + 'index-10.13-10.12-10.11-10.10-10.9' + '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', + '18': 'https://swscan.apple.com/content/catalogs/others/' + 'index-10.14-10.13-10.12-10.11-10.10-10.9' + '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', +} + SEED_CATALOGS_PLIST = ( '/System/Library/PrivateFrameworks/Seeding.framework/Versions/Current/' @@ -99,17 +103,24 @@ def get_seeding_program(sucatalog_url): for key, value in seed_catalogs.items(): if sucatalog_url == value: return key + return '' except (OSError, ExpatError, AttributeError, KeyError): - return None + return '' -def get_seed_catalog(): +def get_seed_catalog(seedname='DeveloperSeed'): '''Returns the developer seed sucatalog''' try: seed_catalogs = plistlib.readPlist(SEED_CATALOGS_PLIST) - return seed_catalogs.get('DeveloperSeed', DEFAULT_SUCATALOG) + return seed_catalogs.get(seedname) except (OSError, ExpatError, AttributeError, KeyError): - return DEFAULT_SUCATALOG + return '' + + +def get_default_catalog(): + '''Returns the default softwareupdate catalog for the current OS''' + darwin_major = os.uname()[2].split('.')[0] + return DEFAULT_SUCATALOGS.get(darwin_major) def make_sparse_image(volume_name, output_path): @@ -207,8 +218,11 @@ class ReplicationError(Exception): pass -def replicate_url(full_url, root_dir='/tmp', - show_progress=False, ignore_cache=False): +def replicate_url(full_url, + root_dir='/tmp', + show_progress=False, + ignore_cache=False, + attempt_resume=False): '''Downloads a URL and stores it in the same relative path on our filesystem. Returns a path to the replicated file.''' @@ -224,6 +238,8 @@ def replicate_url(full_url, root_dir='/tmp', '-o', local_file_path] if not ignore_cache and os.path.exists(local_file_path): curl_cmd.extend(['-z', local_file_path]) + if attempt_resume: + curl_cmd.extend(['-C', '-']) curl_cmd.append(full_url) # print "Downloading %s..." % full_url try: @@ -346,8 +362,8 @@ def download_and_parse_sucatalog(sucatalog, workdir, ignore_cache=False): print >> sys.stderr, 'Could not replicate %s: %s' % (sucatalog, err) exit(-1) if os.path.splitext(localcatalogpath)[1] == '.gz': - with gzip.open(localcatalogpath) as f: - content = f.read() + with gzip.open(localcatalogpath) as the_file: + content = the_file.read() try: catalog = plistlib.readPlistFromString(content) return catalog @@ -430,7 +446,8 @@ def replicate_product(catalog, product_id, workdir, ignore_cache=False): try: replicate_url( package['URL'], root_dir=workdir, - show_progress=True, ignore_cache=ignore_cache) + show_progress=True, ignore_cache=ignore_cache, + attempt_resume=(not ignore_cache)) except ReplicationError, err: print >> sys.stderr, ( 'Could not replicate %s: %s' % (package['URL'], err)) @@ -468,9 +485,12 @@ def main(): 'run again with sudo or as root.') parser = argparse.ArgumentParser() - parser.add_argument('--catalogurl', metavar='sucatalog_url', - default=get_seed_catalog(), - help='Software Update catalog URL.') + parser.add_argument('--seedprogram', default='', + help='Which Seed Program catalog to use. Valid values ' + 'are CustomerSeed, DeveloperSeed, and PublicSeed.') + parser.add_argument('--catalogurl', default='', + help='Software Update catalog URL. This option ' + 'overrides any seedprogram option.') parser.add_argument('--workdir', metavar='path_to_working_dir', default='.', help='Path to working directory on a volume with over ' @@ -519,9 +539,25 @@ def main(): print "%-17s: %s" % ('OS Version', build_info[0]) print "%-17s: %s\n" % ('Build ID', build_info[1]) + if args.catalogurl: + su_catalog_url = args.catalogurl + elif args.seedprogram: + su_catalog_url = get_seed_catalog(args.seedprogram) + if not su_catalog_url: + print >> sys.stderr, ( + 'Could not find a catalog url for seed program %s' + % args.seedprogram) + exit(-1) + else: + su_catalog_url = get_default_catalog() + if not su_catalog_url: + print >> sys.stderr, ( + 'Could not find a default catalog url for this OS version.') + exit(-1) + # download sucatalog and look for products that are for macOS installers catalog = download_and_parse_sucatalog( - args.catalogurl, args.workdir, ignore_cache=args.ignore_cache) + su_catalog_url, args.workdir, ignore_cache=args.ignore_cache) product_info = os_installer_product_info( catalog, args.workdir, ignore_cache=args.ignore_cache)