Restore fetch-macOS.py from OSX-KVM previous
This commit is contained in:
		
							parent
							
								
									2414f466d0
								
							
						
					
					
						commit
						d4ffc1f2f3
					
				|  | @ -165,6 +165,8 @@ RUN patched_glibc=glibc-linux4-2.33-4-x86_64.pkg.tar.zst \ | |||
| 
 | ||||
| WORKDIR /home/arch/OSX-KVM | ||||
| 
 | ||||
| RUN wget https://raw.githubusercontent.com/sickcodes/Docker-OSX/master/fetch-macOS.py | ||||
| 
 | ||||
| RUN [[ "${VERSION%%.*}" -lt 11 ]] && { python fetch-macOS.py --version "${VERSION}" \ | ||||
|         && qemu-img convert BaseSystem.dmg -O qcow2 -p -c BaseSystem.img \ | ||||
|         && qemu-img create -f qcow2 mac_hdd_ng.img "${SIZE}" \ | ||||
|  | @ -340,7 +342,7 @@ CMD sudo touch /dev/kvm /dev/snd "${IMAGE_PATH}" "${BOOTDISK}" "${ENV}" || true | |||
|             --height "${HEIGHT:-1080}" \ | ||||
|             --output-bootdisk "${BOOTDISK:=/home/arch/OSX-KVM/OpenCore-Catalina/OpenCore.qcow2}" \ | ||||
|     ; } \ | ||||
|     ; ./enable-ssh.sh && ./Launch.sh | ||||
|     ; ./enable-ssh.sh && /bin/bash -c ./Launch.sh | ||||
| 
 | ||||
| # virt-manager mode: eta son | ||||
| # CMD virsh define <(envsubst < Docker-OSX.xml) && virt-manager || virt-manager | ||||
|  |  | |||
|  | @ -0,0 +1,447 @@ | |||
| #!/usr/bin/env python3 | ||||
| # encoding: utf-8 | ||||
| # | ||||
| # https://github.com/munki/macadmin-scripts/blob/master/installinstallmacos.py | ||||
| # | ||||
| # Copyright 2017 Greg Neagle. | ||||
| # | ||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| # you may not use this file except in compliance with the License. | ||||
| # You may obtain a copy of the License at | ||||
| # | ||||
| #      http://www.apache.org/licenses/LICENSE-2.0 | ||||
| # | ||||
| # Unless required by applicable law or agreed to in writing, software | ||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| # See the License for the specific language governing permissions and | ||||
| # limitations under the License. | ||||
| # | ||||
| # Thanks to Tim Sutton for ideas, suggestions, and sample code. | ||||
| # | ||||
| # Updated in May of 2019 by Dhiru Kholia. | ||||
| 
 | ||||
| '''installinstallmacos.py | ||||
| A tool to download the parts for an Install macOS app from Apple's | ||||
| softwareupdate servers and install a functioning Install macOS app onto an | ||||
| empty disk image''' | ||||
| 
 | ||||
| # https://github.com/foxlet/macOS-Simple-KVM/blob/master/tools/FetchMacOS/fetch-macos.py | ||||
| # is pretty similar. | ||||
| 
 | ||||
| 
 | ||||
| # Bad hack | ||||
| import warnings | ||||
| 
 | ||||
| warnings.filterwarnings("ignore", category=DeprecationWarning) | ||||
| 
 | ||||
| import os | ||||
| import gzip | ||||
| import argparse | ||||
| import plistlib | ||||
| import subprocess | ||||
| 
 | ||||
| from xml.dom import minidom | ||||
| from xml.parsers.expat import ExpatError | ||||
| 
 | ||||
| 
 | ||||
| import sys | ||||
| 
 | ||||
| if sys.version_info[0] < 3: | ||||
|     import urlparse as urlstuff | ||||
| else: | ||||
|     import urllib.parse as urlstuff | ||||
| # Quick fix for python 3.9 and above | ||||
| if sys.version_info[0] == 3 and sys.version_info[1] >= 9: | ||||
|     from types import MethodType | ||||
| 
 | ||||
|     def readPlist(self,filepath): | ||||
|         with open(filepath, 'rb') as f: | ||||
|             p = plistlib._PlistParser(dict) | ||||
|             rootObject = p.parse(f) | ||||
|         return rootObject | ||||
|     # adding the method readPlist() to plistlib | ||||
|     plistlib.readPlist = MethodType(readPlist, plistlib) | ||||
| 
 | ||||
| # https://github.com/foxlet/macOS-Simple-KVM/blob/master/tools/FetchMacOS/fetch-macos.py (unused) | ||||
| # https://github.com/munki/macadmin-scripts | ||||
| catalogs = { | ||||
|     "CustomerSeed": "https://swscan.apple.com/content/catalogs/others/index-10.16customerseed-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", | ||||
|     "DeveloperSeed": "https://swscan.apple.com/content/catalogs/others/index-10.16seed-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", | ||||
|     "PublicSeed": "https://swscan.apple.com/content/catalogs/others/index-10.16beta-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", | ||||
|     "PublicRelease": "https://swscan.apple.com/content/catalogs/others/index-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog", | ||||
|     "20": "https://swscan.apple.com/content/catalogs/others/index-11-10.15-10.14-10.13-10.12-10.11-10.10-10.9-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog" | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| def get_default_catalog(): | ||||
|     '''Returns the default softwareupdate catalog for the current OS''' | ||||
|     return catalogs["20"] | ||||
|     # return catalogs["PublicRelease"] | ||||
|     # return catalogs["DeveloperSeed"] | ||||
| 
 | ||||
| 
 | ||||
| class ReplicationError(Exception): | ||||
|     '''A custom error when replication fails''' | ||||
|     pass | ||||
| 
 | ||||
| 
 | ||||
| def cmd_exists(cmd): | ||||
|     return subprocess.call("type " + cmd, shell=True, | ||||
|                            stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 | ||||
| 
 | ||||
| 
 | ||||
| def replicate_url(full_url, | ||||
|                   root_dir='/tmp', | ||||
|                   show_progress=False, | ||||
|                   ignore_cache=False, | ||||
|                   attempt_resume=False, installer=False, product_title=""): | ||||
|     '''Downloads a URL and stores it in the same relative path on our | ||||
|     filesystem. Returns a path to the replicated file.''' | ||||
| 
 | ||||
|     # hack | ||||
|     print("[+] Fetching %s" % full_url) | ||||
|     if installer and "BaseSystem.dmg" not in full_url and "Big Sur" not in product_title: | ||||
|         return | ||||
|     if "Big Sur" in product_title and "InstallAssistant.pkg" not in full_url: | ||||
|         return | ||||
|     attempt_resume = True | ||||
|     # path = urllib.parse.urlsplit(full_url)[2] | ||||
|     path = urlstuff.urlsplit(full_url)[2] | ||||
|     relative_url = path.lstrip('/') | ||||
|     relative_url = os.path.normpath(relative_url) | ||||
|     # local_file_path = os.path.join(root_dir, relative_url) | ||||
|     local_file_path = relative_url | ||||
|     # print("Downloading %s..." % full_url) | ||||
| 
 | ||||
|     if cmd_exists('wget'): | ||||
|         if not installer: | ||||
|             download_cmd = ['wget', "-c", "--quiet", "-x", "-nH", full_url] | ||||
|             # this doesn't work as there are multiple metadata files with the same name! | ||||
|             # download_cmd = ['wget', "-c", "--quiet", full_url] | ||||
|         else: | ||||
|             download_cmd = ['wget', "-c", full_url] | ||||
|     else: | ||||
|         if not installer: | ||||
|             download_cmd = ['curl', "--silent", "--show-error", "-o", local_file_path, "--create-dirs", full_url] | ||||
|         else: | ||||
|             local_file_path = os.path.basename(local_file_path) | ||||
|             download_cmd = ['curl', "-o", local_file_path, full_url] | ||||
| 
 | ||||
|     try: | ||||
|         subprocess.check_call(download_cmd) | ||||
|     except subprocess.CalledProcessError as err: | ||||
|         raise ReplicationError(err) | ||||
|     return local_file_path | ||||
| 
 | ||||
| 
 | ||||
| def parse_server_metadata(filename): | ||||
|     '''Parses a softwareupdate server metadata file, looking for information | ||||
|     of interest. | ||||
|     Returns a dictionary containing title, version, and description.''' | ||||
|     title = '' | ||||
|     vers = '' | ||||
|     try: | ||||
|         md_plist = plistlib.readPlist(filename) | ||||
|     except (OSError, IOError, ExpatError) as err: | ||||
|         print('Error reading %s: %s' % (filename, err), file=sys.stderr) | ||||
|         return {} | ||||
|     vers = md_plist.get('CFBundleShortVersionString', '') | ||||
|     localization = md_plist.get('localization', {}) | ||||
|     preferred_localization = (localization.get('English') or | ||||
|                               localization.get('en')) | ||||
|     if preferred_localization: | ||||
|         title = preferred_localization.get('title', '') | ||||
| 
 | ||||
|     metadata = {} | ||||
|     metadata['title'] = title | ||||
|     metadata['version'] = vers | ||||
| 
 | ||||
|     """ | ||||
|     {'title': 'macOS Mojave', 'version': '10.14.5'} | ||||
|     {'title': 'macOS Mojave', 'version': '10.14.6'} | ||||
|     """ | ||||
|     return metadata | ||||
| 
 | ||||
| 
 | ||||
| def get_server_metadata(catalog, product_key, workdir, ignore_cache=False): | ||||
|     '''Replicate ServerMetaData''' | ||||
|     try: | ||||
|         url = catalog['Products'][product_key]['ServerMetadataURL'] | ||||
|         try: | ||||
|             smd_path = replicate_url( | ||||
|                 url, root_dir=workdir, ignore_cache=ignore_cache) | ||||
|             return smd_path | ||||
|         except ReplicationError as err: | ||||
|             print('Could not replicate %s: %s' % (url, err), file=sys.stderr) | ||||
|             return None | ||||
|     except KeyError: | ||||
|         # print('Malformed catalog.', file=sys.stderr) | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| def parse_dist(filename): | ||||
|     '''Parses a softwareupdate dist file, returning a dict of info of | ||||
|     interest''' | ||||
|     dist_info = {} | ||||
|     try: | ||||
|         dom = minidom.parse(filename) | ||||
|     except ExpatError: | ||||
|         print('Invalid XML in %s' % filename, file=sys.stderr) | ||||
|         return dist_info | ||||
|     except IOError as err: | ||||
|         print('Error reading %s: %s' % (filename, err), file=sys.stderr) | ||||
|         return dist_info | ||||
| 
 | ||||
|     titles = dom.getElementsByTagName('title') | ||||
|     if titles: | ||||
|         dist_info['title_from_dist'] = titles[0].firstChild.wholeText | ||||
| 
 | ||||
|     auxinfos = dom.getElementsByTagName('auxinfo') | ||||
|     if not auxinfos: | ||||
|         return dist_info | ||||
|     auxinfo = auxinfos[0] | ||||
|     key = None | ||||
|     value = None | ||||
|     children = auxinfo.childNodes | ||||
|     # handle the possibility that keys from auxinfo may be nested | ||||
|     # within a 'dict' element | ||||
|     dict_nodes = [n for n in auxinfo.childNodes | ||||
|                   if n.nodeType == n.ELEMENT_NODE and | ||||
|                   n.tagName == 'dict'] | ||||
|     if dict_nodes: | ||||
|         children = dict_nodes[0].childNodes | ||||
|     for node in children: | ||||
|         if node.nodeType == node.ELEMENT_NODE and node.tagName == 'key': | ||||
|             key = node.firstChild.wholeText | ||||
|         if node.nodeType == node.ELEMENT_NODE and node.tagName == 'string': | ||||
|             value = node.firstChild.wholeText | ||||
|         if key and value: | ||||
|             dist_info[key] = value | ||||
|             key = None | ||||
|             value = None | ||||
|     return dist_info | ||||
| 
 | ||||
| 
 | ||||
| def download_and_parse_sucatalog(sucatalog, workdir, ignore_cache=False): | ||||
|     '''Downloads and returns a parsed softwareupdate catalog''' | ||||
|     try: | ||||
|         localcatalogpath = replicate_url( | ||||
|             sucatalog, root_dir=workdir, ignore_cache=ignore_cache) | ||||
|     except ReplicationError as err: | ||||
|         print('Could not replicate %s: %s' % (sucatalog, err), file=sys.stderr) | ||||
|         exit(-1) | ||||
|     if os.path.splitext(localcatalogpath)[1] == '.gz': | ||||
|         with gzip.open(localcatalogpath) as the_file: | ||||
|             content = the_file.read() | ||||
|             try: | ||||
|                 catalog = plistlib.readPlistFromString(content) | ||||
|                 return catalog | ||||
|             except ExpatError as err: | ||||
|                 print('Error reading %s: %s' % (localcatalogpath, err), file=sys.stderr) | ||||
|                 exit(-1) | ||||
|     else: | ||||
|         try: | ||||
|             catalog = plistlib.readPlist(localcatalogpath) | ||||
|             return catalog | ||||
|         except (OSError, IOError, ExpatError) as err: | ||||
|             print('Error reading %s: %s' % (localcatalogpath, err), file=sys.stderr) | ||||
|             exit(-1) | ||||
| 
 | ||||
| 
 | ||||
| def find_mac_os_installers(catalog): | ||||
|     '''Return a list of product identifiers for what appear to be macOS | ||||
|     installers''' | ||||
|     mac_os_installer_products = [] | ||||
|     if 'Products' in catalog: | ||||
|         for product_key in catalog['Products'].keys(): | ||||
|             product = catalog['Products'][product_key] | ||||
|             try: | ||||
|                 if product['ExtendedMetaInfo'][ | ||||
|                         'InstallAssistantPackageIdentifiers']: | ||||
|                     mac_os_installer_products.append(product_key) | ||||
|             except KeyError: | ||||
|                 continue | ||||
| 
 | ||||
|     return mac_os_installer_products | ||||
| 
 | ||||
| 
 | ||||
| def os_installer_product_info(catalog, workdir, ignore_cache=False): | ||||
|     '''Returns a dict of info about products that look like macOS installers''' | ||||
|     product_info = {} | ||||
|     installer_products = find_mac_os_installers(catalog) | ||||
|     for product_key in installer_products: | ||||
|         product_info[product_key] = {} | ||||
|         filename = get_server_metadata(catalog, product_key, workdir) | ||||
|         if filename: | ||||
|             product_info[product_key] = parse_server_metadata(filename) | ||||
|         else: | ||||
|             # print('No server metadata for %s' % product_key) | ||||
|             product_info[product_key]['title'] = None | ||||
|             product_info[product_key]['version'] = None | ||||
| 
 | ||||
|         product = catalog['Products'][product_key] | ||||
|         product_info[product_key]['PostDate'] = product['PostDate'] | ||||
|         distributions = product['Distributions'] | ||||
|         dist_url = distributions.get('English') or distributions.get('en') | ||||
|         try: | ||||
|             dist_path = replicate_url( | ||||
|                 dist_url, root_dir=workdir, ignore_cache=ignore_cache) | ||||
|         except ReplicationError as err: | ||||
|             print('Could not replicate %s: %s' % (dist_url, err), | ||||
|                   file=sys.stderr) | ||||
|         else: | ||||
|             dist_info = parse_dist(dist_path) | ||||
|             product_info[product_key]['DistributionPath'] = dist_path | ||||
|             product_info[product_key].update(dist_info) | ||||
|             if not product_info[product_key]['title']: | ||||
|                 product_info[product_key]['title'] = dist_info.get('title_from_dist') | ||||
|             if not product_info[product_key]['version']: | ||||
|                 product_info[product_key]['version'] = dist_info.get('VERSION') | ||||
| 
 | ||||
|     return product_info | ||||
| 
 | ||||
| 
 | ||||
| def replicate_product(catalog, product_id, workdir, ignore_cache=False, product_title=""): | ||||
|     '''Downloads all the packages for a product''' | ||||
|     product = catalog['Products'][product_id] | ||||
|     for package in product.get('Packages', []): | ||||
|         # TO-DO: Check 'Size' attribute and make sure | ||||
|         # we have enough space on the target | ||||
|         # filesystem before attempting to download | ||||
|         if 'URL' in package: | ||||
|             try: | ||||
|                 replicate_url( | ||||
|                     package['URL'], root_dir=workdir, | ||||
|                     show_progress=True, ignore_cache=ignore_cache, | ||||
|                     attempt_resume=(not ignore_cache), installer=True, product_title=product_title) | ||||
|             except ReplicationError as err: | ||||
|                 print('Could not replicate %s: %s' % (package['URL'], err), file=sys.stderr) | ||||
|                 exit(-1) | ||||
|         if 'MetadataURL' in package: | ||||
|             try: | ||||
|                 replicate_url(package['MetadataURL'], root_dir=workdir, | ||||
|                               ignore_cache=ignore_cache, installer=True) | ||||
|             except ReplicationError as err: | ||||
|                 print('Could not replicate %s: %s' % (package['MetadataURL'], err), file=sys.stderr) | ||||
|                 exit(-1) | ||||
| 
 | ||||
| 
 | ||||
| def find_installer_app(mountpoint): | ||||
|     '''Returns the path to the Install macOS app on the mountpoint''' | ||||
|     applications_dir = os.path.join(mountpoint, 'Applications') | ||||
|     for item in os.listdir(applications_dir): | ||||
|         if item.endswith('.app'): | ||||
|             return os.path.join(applications_dir, item) | ||||
|     return None | ||||
| 
 | ||||
| 
 | ||||
| def determine_version(version, product_info): | ||||
|     if version: | ||||
|         if version == 'latest': | ||||
|             from distutils.version import StrictVersion | ||||
|             latest_version = StrictVersion('0.0.0') | ||||
|             for index, product_id in enumerate(product_info): | ||||
|                 d = product_info[product_id]['version'] | ||||
|                 if d > latest_version: | ||||
|                     latest_version = d | ||||
| 
 | ||||
|             if latest_version == StrictVersion("0.0.0"): | ||||
|                 print("Could not find latest version {}") | ||||
|                 exit(1) | ||||
| 
 | ||||
|             version = str(latest_version) | ||||
| 
 | ||||
|         for index, product_id in enumerate(product_info): | ||||
|             v = product_info[product_id]['version'] | ||||
|             if v == version: | ||||
|                 return product_id, product_info[product_id]['title'] | ||||
| 
 | ||||
|         print("Could not find version {}. Versions available are:".format(version)) | ||||
|         for _, pid in enumerate(product_info): | ||||
|             print("- {}".format(product_info[pid]['version'])) | ||||
| 
 | ||||
|         exit(1) | ||||
| 
 | ||||
|     # display a menu of choices (some seed catalogs have multiple installers) | ||||
|     print('%2s %12s %10s %11s  %s' % ('#', 'ProductID', 'Version', | ||||
|                                            'Post Date', 'Title')) | ||||
|     for index, product_id in enumerate(product_info): | ||||
|         print('%2s %12s %10s %11s  %s' % ( | ||||
|             index + 1, | ||||
|             product_id, | ||||
|             product_info[product_id]['version'], | ||||
|             product_info[product_id]['PostDate'].strftime('%Y-%m-%d'), | ||||
|             product_info[product_id]['title'] | ||||
|         )) | ||||
| 
 | ||||
|     answer = input( | ||||
|         '\nChoose a product to download (1-%s): ' % len(product_info)) | ||||
|     try: | ||||
|         index = int(answer) - 1 | ||||
|         if index < 0: | ||||
|             raise ValueError | ||||
|         product_id = list(product_info.keys())[index] | ||||
|         return product_id, product_info[product_id]['title'] | ||||
|     except (ValueError, IndexError): | ||||
|         pass | ||||
| 
 | ||||
|     print('Invalid input provided.') | ||||
|     exit(0) | ||||
| 
 | ||||
| 
 | ||||
| def main(): | ||||
|     '''Do the main thing here''' | ||||
|     """ | ||||
|     if os.getuid() != 0: | ||||
|         sys.exit('This command requires root (to install packages), so please ' | ||||
|                  'run again with sudo or as root.') | ||||
|     """ | ||||
|     parser = argparse.ArgumentParser() | ||||
|     parser.add_argument('--workdir', metavar='path_to_working_dir', | ||||
|                         default='.', | ||||
|                         help='Path to working directory on a volume with over ' | ||||
|                              '10G of available space. Defaults to current working ' | ||||
|                              'directory.') | ||||
|     parser.add_argument('--version', metavar='version', | ||||
|                         default=None, | ||||
|                         help='The version to download in the format of ' | ||||
|                              '"$major.$minor.$patch", e.g. "10.15.4". Can ' | ||||
|                              'be "latest" to download the latest version.') | ||||
|     parser.add_argument('--compress', action='store_true', | ||||
|                         help='Output a read-only compressed disk image with ' | ||||
|                              'the Install macOS app at the root. This is now the ' | ||||
|                              'default. Use --raw to get a read-write sparse image ' | ||||
|                              'with the app in the Applications directory.') | ||||
|     parser.add_argument('--raw', action='store_true', | ||||
|                         help='Output a read-write sparse image ' | ||||
|                              'with the app in the Applications directory. Requires ' | ||||
|                              'less available disk space and is faster.') | ||||
|     parser.add_argument('--ignore-cache', action='store_true', | ||||
|                         help='Ignore any previously cached files.') | ||||
|     args = parser.parse_args() | ||||
| 
 | ||||
|     su_catalog_url = get_default_catalog() | ||||
|     if not su_catalog_url: | ||||
|         print('Could not find a default catalog url for this OS version.', file=sys.stderr) | ||||
|         exit(-1) | ||||
| 
 | ||||
|     # download sucatalog and look for products that are for macOS installers | ||||
|     catalog = download_and_parse_sucatalog( | ||||
|         su_catalog_url, args.workdir, ignore_cache=args.ignore_cache) | ||||
|     product_info = os_installer_product_info( | ||||
|         catalog, args.workdir, ignore_cache=args.ignore_cache) | ||||
| 
 | ||||
|     if not product_info: | ||||
|         print('No macOS installer products found in the sucatalog.', file=sys.stderr) | ||||
|         exit(-1) | ||||
| 
 | ||||
|     product_id, product_title = determine_version(args.version, product_info) | ||||
|     print(product_id, product_title) | ||||
| 
 | ||||
|     # download all the packages for the selected product | ||||
|     replicate_product(catalog, product_id, args.workdir, ignore_cache=args.ignore_cache, product_title=product_title) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
		Loading…
	
		Reference in New Issue