This commit is contained in:
G Pugh 2019-07-04 10:49:28 +02:00
commit 57dd500dec

View File

@ -23,6 +23,9 @@ 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 softwareupdate servers and install a functioning Install macOS app onto an
empty disk image''' empty disk image'''
# Python 3 compatibility shims
from __future__ import (
absolute_import, division, print_function, unicode_literals)
import argparse import argparse
import gzip import gzip
@ -30,8 +33,13 @@ import os
import plistlib import plistlib
import subprocess import subprocess
import sys import sys
import urlparse
import xattr import xattr
try:
# python 2
from urllib.parse import urlsplit
except ImportError:
# python 3
from urlparse import urlsplit
from xml.dom import minidom from xml.dom import minidom
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
from distutils.version import LooseVersion from distutils.version import LooseVersion
@ -109,10 +117,38 @@ def get_current_build_info():
return build_info return build_info
def get_input(prompt=None):
'''Python 2 and 3 wrapper for raw_input/input'''
try:
return raw_input(prompt)
except NameError:
# raw_input doesn't exist in Python 3
return input(prompt)
def read_plist(filepath):
'''Wrapper for the differences between Python 2 and Python 3's plistlib'''
try:
with open(filepath, "rb") as fileobj:
return plistlib.load(fileobj)
except AttributeError:
# plistlib module doesn't have a load function (as in Python 2)
return plistlib.readPlist(filepath)
def read_plist_from_string(bytestring):
'''Wrapper for the differences between Python 2 and Python 3's plistlib'''
try:
return plistlib.loads(bytestring)
except AttributeError:
# plistlib module doesn't have a load function (as in Python 2)
return plistlib.readPlistFromString(bytestring)
def get_seeding_program(sucatalog_url): def get_seeding_program(sucatalog_url):
'''Returns a seeding program name based on the sucatalog_url''' '''Returns a seeding program name based on the sucatalog_url'''
try: try:
seed_catalogs = plistlib.readPlist(SEED_CATALOGS_PLIST) seed_catalogs = read_plist(SEED_CATALOGS_PLIST)
for key, value in seed_catalogs.items(): for key, value in seed_catalogs.items():
if sucatalog_url == value: if sucatalog_url == value:
return key return key
@ -124,7 +160,7 @@ def get_seeding_program(sucatalog_url):
def get_seed_catalog(seedname='DeveloperSeed'): def get_seed_catalog(seedname='DeveloperSeed'):
'''Returns the developer seed sucatalog''' '''Returns the developer seed sucatalog'''
try: try:
seed_catalogs = plistlib.readPlist(SEED_CATALOGS_PLIST) seed_catalogs = read_plist(SEED_CATALOGS_PLIST)
return seed_catalogs.get(seedname) return seed_catalogs.get(seedname)
except (OSError, ExpatError, AttributeError, KeyError): except (OSError, ExpatError, AttributeError, KeyError):
return '' return ''
@ -133,8 +169,8 @@ def get_seed_catalog(seedname='DeveloperSeed'):
def get_seeding_programs(): def get_seeding_programs():
'''Returns the list of seeding program names''' '''Returns the list of seeding program names'''
try: try:
seed_catalogs = plistlib.readPlist(SEED_CATALOGS_PLIST) seed_catalogs = read_plist(SEED_CATALOGS_PLIST)
return seed_catalogs.keys() return list(seed_catalogs.keys())
except (OSError, ExpatError, AttributeError, KeyError): except (OSError, ExpatError, AttributeError, KeyError):
return '' return ''
@ -151,17 +187,17 @@ def make_sparse_image(volume_name, output_path):
'-volname', volume_name, '-type', 'SPARSE', '-plist', output_path] '-volname', volume_name, '-type', 'SPARSE', '-plist', output_path]
try: try:
output = subprocess.check_output(cmd) output = subprocess.check_output(cmd)
except subprocess.CalledProcessError, err: except subprocess.CalledProcessError as err:
print >> sys.stderr, err print(err, file=sys.stderr)
exit(-1) exit(-1)
try: try:
return plistlib.readPlistFromString(output)[0] return read_plist_from_string(output)[0]
except IndexError, err: except IndexError as err:
print >> sys.stderr, 'Unexpected output from hdiutil: %s' % output print('Unexpected output from hdiutil: %s' % output, file=sys.stderr)
exit(-1) exit(-1)
except ExpatError, err: except ExpatError as err:
print >> sys.stderr, 'Malformed output from hdiutil: %s' % output print('Malformed output from hdiutil: %s' % output, file=sys.stderr)
print >> sys.stderr, err print(err, file=sys.stderr)
exit(-1) exit(-1)
@ -169,16 +205,16 @@ def make_compressed_dmg(app_path, diskimagepath, volume_name):
"""Returns path to newly-created compressed r/o disk image containing """Returns path to newly-created compressed r/o disk image containing
Install macOS.app""" Install macOS.app"""
print ('Making read-only compressed disk image containing %s...' print('Making read-only compressed disk image containing %s...'
% os.path.basename(app_path)) % os.path.basename(app_path))
cmd = ['/usr/bin/hdiutil', 'create', '-volname', volume_name, '-fs', 'HFS+', cmd = ['/usr/bin/hdiutil', 'create', '-fs', 'HFS+',
'-srcfolder', app_path, diskimagepath] '-srcfolder', app_path, diskimagepath]
try: try:
subprocess.check_call(cmd) subprocess.check_call(cmd)
except subprocess.CalledProcessError, err: except subprocess.CalledProcessError as err:
print >> sys.stderr, err print(err, file=sys.stderr)
else: else:
print 'Disk image created at: %s' % diskimagepath print('Disk image created at: %s' % diskimagepath)
def mountdmg(dmgpath): def mountdmg(dmgpath):
@ -194,10 +230,11 @@ def mountdmg(dmgpath):
stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(pliststr, err) = proc.communicate() (pliststr, err) = proc.communicate()
if proc.returncode: if proc.returncode:
print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname) print('Error: "%s" while mounting %s.' % (err, dmgname),
file=sys.stderr)
return None return None
if pliststr: if pliststr:
plist = plistlib.readPlistFromString(pliststr) plist = read_plist_from_string(pliststr)
for entity in plist['system-entities']: for entity in plist['system-entities']:
if 'mount-point' in entity: if 'mount-point' in entity:
mountpoints.append(entity['mount-point']) mountpoints.append(entity['mount-point'])
@ -214,13 +251,13 @@ def unmountdmg(mountpoint):
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
(dummy_output, err) = proc.communicate() (dummy_output, err) = proc.communicate()
if proc.returncode: if proc.returncode:
print >> sys.stderr, 'Polite unmount failed: %s' % err print('Polite unmount failed: %s' % err, file=sys.stderr)
print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint print('Attempting to force unmount %s' % mountpoint, file=sys.stderr)
# try forcing the unmount # try forcing the unmount
retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint, retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint,
'-force']) '-force'])
if retcode: if retcode:
print >> sys.stderr, 'Failed to unmount %s' % mountpoint print('Failed to unmount %s' % mountpoint, file=sys.stderr)
def install_product(dist_path, target_vol): def install_product(dist_path, target_vol):
@ -230,8 +267,8 @@ def install_product(dist_path, target_vol):
try: try:
subprocess.check_call(cmd) subprocess.check_call(cmd)
return True return True
except subprocess.CalledProcessError, err: except subprocess.CalledProcessError as err:
print >> sys.stderr, err print(err, file=sys.stderr)
return False return False
@ -248,7 +285,7 @@ def replicate_url(full_url,
'''Downloads a URL and stores it in the same relative path on our '''Downloads a URL and stores it in the same relative path on our
filesystem. Returns a path to the replicated file.''' filesystem. Returns a path to the replicated file.'''
path = urlparse.urlsplit(full_url)[2] path = urlsplit(full_url)[2]
relative_url = path.lstrip('/') relative_url = path.lstrip('/')
relative_url = os.path.normpath(relative_url) relative_url = os.path.normpath(relative_url)
local_file_path = os.path.join(root_dir, relative_url) local_file_path = os.path.join(root_dir, relative_url)
@ -263,10 +300,10 @@ def replicate_url(full_url,
if attempt_resume: if attempt_resume:
curl_cmd.extend(['-C', '-']) curl_cmd.extend(['-C', '-'])
curl_cmd.append(full_url) curl_cmd.append(full_url)
# print "Downloading %s..." % full_url # print("Downloading %s..." % full_url)
try: try:
subprocess.check_call(curl_cmd) subprocess.check_call(curl_cmd)
except subprocess.CalledProcessError, err: except subprocess.CalledProcessError as err:
raise ReplicationError(err) raise ReplicationError(err)
return local_file_path return local_file_path
@ -278,9 +315,9 @@ def parse_server_metadata(filename):
title = '' title = ''
vers = '' vers = ''
try: try:
md_plist = plistlib.readPlist(filename) md_plist = read_plist(filename)
except (OSError, IOError, ExpatError), err: except (OSError, IOError, ExpatError) as err:
print >> sys.stderr, 'Error reading %s: %s' % (filename, err) print('Error reading %s: %s' % (filename, err), file=sys.stderr)
return {} return {}
vers = md_plist.get('CFBundleShortVersionString', '') vers = md_plist.get('CFBundleShortVersionString', '')
localization = md_plist.get('localization', {}) localization = md_plist.get('localization', {})
@ -303,12 +340,11 @@ def get_server_metadata(catalog, product_key, workdir, ignore_cache=False):
smd_path = replicate_url( smd_path = replicate_url(
url, root_dir=workdir, ignore_cache=ignore_cache) url, root_dir=workdir, ignore_cache=ignore_cache)
return smd_path return smd_path
except ReplicationError, err: except ReplicationError as err:
print >> sys.stderr, ( print('Could not replicate %s: %s' % (url, err), file=sys.stderr)
'Could not replicate %s: %s' % (url, err))
return None return None
except KeyError: except KeyError:
print >> sys.stderr, 'Malformed catalog.' print('Malformed catalog.', file=sys.stderr)
return None return None
@ -319,10 +355,10 @@ def parse_dist(filename):
try: try:
dom = minidom.parse(filename) dom = minidom.parse(filename)
except ExpatError: except ExpatError:
print >> sys.stderr, 'Invalid XML in %s' % filename print('Invalid XML in %s' % filename, file=sys.stderr)
return dist_info return dist_info
except IOError, err: except IOError as err:
print >> sys.stderr, 'Error reading %s: %s' % (filename, err) print('Error reading %s: %s' % (filename, err), file=sys.stderr)
return dist_info return dist_info
auxinfos = dom.getElementsByTagName('auxinfo') auxinfos = dom.getElementsByTagName('auxinfo')
@ -380,26 +416,26 @@ def download_and_parse_sucatalog(sucatalog, workdir, ignore_cache=False):
try: try:
localcatalogpath = replicate_url( localcatalogpath = replicate_url(
sucatalog, root_dir=workdir, ignore_cache=ignore_cache) sucatalog, root_dir=workdir, ignore_cache=ignore_cache)
except ReplicationError, err: except ReplicationError as err:
print >> sys.stderr, 'Could not replicate %s: %s' % (sucatalog, err) print('Could not replicate %s: %s' % (sucatalog, err), file=sys.stderr)
exit(-1) exit(-1)
if os.path.splitext(localcatalogpath)[1] == '.gz': if os.path.splitext(localcatalogpath)[1] == '.gz':
with gzip.open(localcatalogpath) as the_file: with gzip.open(localcatalogpath) as the_file:
content = the_file.read() content = the_file.read()
try: try:
catalog = plistlib.readPlistFromString(content) catalog = read_plist_from_string(content)
return catalog return catalog
except ExpatError, err: except ExpatError as err:
print >> sys.stderr, ( print('Error reading %s: %s' % (localcatalogpath, err),
'Error reading %s: %s' % (localcatalogpath, err)) file=sys.stderr)
exit(-1) exit(-1)
else: else:
try: try:
catalog = plistlib.readPlist(localcatalogpath) catalog = read_plist(localcatalogpath)
return catalog return catalog
except (OSError, IOError, ExpatError), err: except (OSError, IOError, ExpatError) as err:
print >> sys.stderr, ( print('Error reading %s: %s' % (localcatalogpath, err),
'Error reading %s: %s' % (localcatalogpath, err)) file=sys.stderr)
exit(-1) exit(-1)
@ -408,8 +444,7 @@ def find_mac_os_installers(catalog):
installers''' installers'''
mac_os_installer_products = [] mac_os_installer_products = []
if 'Products' in catalog: if 'Products' in catalog:
product_keys = list(catalog['Products'].keys()) for product_key in catalog['Products'].keys():
for product_key in product_keys:
product = catalog['Products'][product_key] product = catalog['Products'][product_key]
try: try:
if product['ExtendedMetaInfo'][ if product['ExtendedMetaInfo'][
@ -436,16 +471,17 @@ def os_installer_product_info(catalog, workdir, ignore_cache=False):
try: try:
dist_path = replicate_url( dist_path = replicate_url(
dist_url, root_dir=workdir, ignore_cache=ignore_cache) dist_url, root_dir=workdir, ignore_cache=ignore_cache)
except ReplicationError, err: except ReplicationError as err:
print >> sys.stderr, 'Could not replicate %s: %s' % (dist_url, err) print('Could not replicate %s: %s' % (dist_url, err),
dist_info = parse_dist(dist_path) file=sys.stderr)
product_info[product_key]['DistributionPath'] = dist_path else:
unsupported_models = get_unsupported_models(dist_path) dist_info = parse_dist(dist_path)
product_info[product_key]['UnsupportedModels'] = unsupported_models product_info[product_key]['DistributionPath'] = dist_path
board_ids = get_board_ids(dist_path) unsupported_models = get_unsupported_models(dist_path)
product_info[product_key]['BoardIDs'] = board_ids product_info[product_key]['UnsupportedModels'] = unsupported_models
product_info[product_key].update(dist_info) board_ids = get_board_ids(dist_path)
product_info[product_key]['BoardIDs'] = board_ids
product_info[product_key].update(dist_info)
return product_info return product_info
@ -470,18 +506,17 @@ def replicate_product(catalog, product_id, workdir, ignore_cache=False):
package['URL'], root_dir=workdir, package['URL'], root_dir=workdir,
show_progress=True, ignore_cache=ignore_cache, show_progress=True, ignore_cache=ignore_cache,
attempt_resume=(not ignore_cache)) attempt_resume=(not ignore_cache))
except ReplicationError, err: except ReplicationError as err:
print >> sys.stderr, ( print('Could not replicate %s: %s' % (package['URL'], err),
'Could not replicate %s: %s' % (package['URL'], err)) file=sys.stderr)
exit(-1) exit(-1)
if 'MetadataURL' in package: if 'MetadataURL' in package:
try: try:
replicate_url(package['MetadataURL'], root_dir=workdir, replicate_url(package['MetadataURL'], root_dir=workdir,
ignore_cache=ignore_cache) ignore_cache=ignore_cache)
except ReplicationError, err: except ReplicationError as err:
print >> sys.stderr, ( print('Could not replicate %s: %s'
'Could not replicate %s: %s' % (package['MetadataURL'], err), file=sys.stderr)
% (package['MetadataURL'], err))
exit(-1) exit(-1)
@ -561,31 +596,29 @@ def main():
build_info = get_current_build_info() build_info = get_current_build_info()
is_vm = is_a_vm() is_vm = is_a_vm()
print "This Mac:" print('This Mac:')
if is_vm == True: if is_vm == True:
print "Identified as a Virtual Machine" print('Identified as a Virtual Machine')
print "%-17s: %s" % ('Model Identifier', hw_model) print('%-17s: %s' % ('Model Identifier', hw_model))
print "%-17s: %s" % ('Board ID', board_id) print('%-17s: %s' % ('Board ID', board_id))
print "%-17s: %s" % ('OS Version', build_info[0]) print('%-17s: %s' % ('OS Version', build_info[0]))
print "%-17s: %s\n" % ('Build ID', build_info[1]) print('%-17s: %s\n' % ('Build ID', build_info[1]))
if args.catalogurl: if args.catalogurl:
su_catalog_url = args.catalogurl su_catalog_url = args.catalogurl
elif args.seedprogram: elif args.seedprogram:
su_catalog_url = get_seed_catalog(args.seedprogram) su_catalog_url = get_seed_catalog(args.seedprogram)
if not su_catalog_url: if not su_catalog_url:
print >> sys.stderr, ( print('Could not find a catalog url for seed program %s'
'Could not find a catalog url for seed program %s' % args.seedprogram, file=sys.stderr)
% args.seedprogram) print('Valid seeding programs are: %s'
print >> sys.stderr, ( % ', '.join(get_seeding_programs()), file=sys.stderr)
'Valid seeding programs are: %s'
% ', '.join(get_seeding_programs()))
exit(-1) exit(-1)
else: else:
su_catalog_url = get_default_catalog() su_catalog_url = get_default_catalog()
if not su_catalog_url: if not su_catalog_url:
print >> sys.stderr, ( print('Could not find a default catalog url for this OS version.',
'Could not find a default catalog url for this OS version.') file=sys.stderr)
exit(-1) exit(-1)
# download sucatalog and look for products that are for macOS installers # download sucatalog and look for products that are for macOS installers
@ -595,8 +628,8 @@ def main():
catalog, args.workdir, ignore_cache=args.ignore_cache) catalog, args.workdir, ignore_cache=args.ignore_cache)
if not product_info: if not product_info:
print >> sys.stderr, ( print('No macOS installer products found in the sucatalog.',
'No macOS installer products found in the sucatalog.') file=sys.stderr)
exit(-1) exit(-1)
output_plist = "%s/softwareupdate.plist" % args.workdir output_plist = "%s/softwareupdate.plist" % args.workdir
@ -606,8 +639,8 @@ def main():
valid_build_found = False valid_build_found = False
# display a menu of choices (some seed catalogs have multiple installers) # display a menu of choices (some seed catalogs have multiple installers)
print '%2s %-15s %-10s %-8s %-11s %-30s %s' % ('#', 'ProductID', 'Version', print('%2s %-15s %-10s %-8s %-11s %-30s %s' % ('#', 'ProductID', 'Version',
'Build', 'Post Date', 'Title', 'Notes') 'Build', 'Post Date', 'Title', 'Notes'))
for index, product_id in enumerate(product_info): for index, product_id in enumerate(product_info):
not_valid = '' not_valid = ''
if hw_model in product_info[product_id]['UnsupportedModels'] and is_vm == False: if hw_model in product_info[product_id]['UnsupportedModels'] and is_vm == False:
@ -619,7 +652,7 @@ def main():
else: else:
valid_build_found = True valid_build_found = True
print '%2s %-15s %-10s %-8s %-11s %-30s %s' % ( print('%2s %-15s %-10s %-8s %-11s %-30s %s' % (
index + 1, index + 1,
product_id, product_id,
product_info[product_id]['version'], product_info[product_id]['version'],
@ -627,7 +660,7 @@ def main():
product_info[product_id]['PostDate'].strftime('%Y-%m-%d'), product_info[product_id]['PostDate'].strftime('%Y-%m-%d'),
product_info[product_id]['title'], product_info[product_id]['title'],
not_valid not_valid
) ))
# go through various options for automatically determining the answer: # go through various options for automatically determining the answer:
@ -648,7 +681,7 @@ def main():
continue continue
# determine the lowest valid build ID and select this # determine the lowest valid build ID and select this
# when using auto and version options # when using auto and version options
if (args.auto or args.version or args.os) and 'Beta' not in product_info[product_id]['title']: if (args.auto or args.version or args.os) and 'Beta' not in product_info[product_id]['title']:
try: try:
lowest_valid_build lowest_valid_build
@ -685,13 +718,13 @@ def main():
# Stop here if no valid builds found # Stop here if no valid builds found
if valid_build_found == False: if valid_build_found == False:
print 'No valid build found for this hardware' print('No valid build found for this hardware')
exit(0) exit(0)
# Output a plist of available updates and quit if list option chosen # Output a plist of available updates and quit if list option chosen
if args.list: if args.list:
plistlib.writePlist(pl, output_plist) plistlib.writePlist(pl, output_plist)
print ('\n' print('\n'
'Valid seeding programs are: %s' 'Valid seeding programs are: %s'
% ', '.join(get_seeding_programs())) % ', '.join(get_seeding_programs()))
exit(0) exit(0)
@ -701,35 +734,41 @@ def main():
try: try:
answer answer
except NameError: except NameError:
print ('\n' print('\n'
'Build %s is not available. ' 'Build %s is not available. '
'Run again without --build argument ' 'Run again without --build argument '
'to select a valid build to download.\n' % args.build) 'to select a valid build to download.\n' % args.build)
exit(0) exit(0)
else: else:
print '\nBuild %s available. Downloading #%s...\n' % (args.build, answer) print('\n'
'Build %s available. Downloading #%s...\n'
% (args.build, answer))
elif args.current: elif args.current:
try: try:
answer answer
except NameError: except NameError:
print ('\n' print('\n'
'Build %s is not available. ' 'Build %s is not available. '
'Run again without --current argument ' 'Run again without --current argument '
'to select a valid build to download.\n' % build_info[0]) 'to select a valid build to download.\n' % build_info[0])
exit(0) exit(0)
else: else:
print '\nBuild %s available. Downloading #%s...\n' % (build_info[0], answer) print('\n'
'Build %s available. Downloading #%s...\n'
% (build_info[0], answer))
elif args.version: elif args.version:
try: try:
answer answer
except NameError: except NameError:
print ('\n' print('\n'
'Item # %s is not available. ' 'Item # %s is not available. '
'Run again without --version argument ' 'Run again without --version argument '
'to select a valid build to download.\n' % args.version) 'to select a valid build to download.\n' % args.version)
exit(0) exit(0)
else: else:
print '\nBuild %s selected. Downloading #%s...\n' % (lowest_valid_build, answer) print('\n'
'Build %s selected. Downloading #%s...\n'
% (lowest_valid_build, answer))
elif args.os: elif args.os:
try: try:
answer answer
@ -740,30 +779,34 @@ def main():
'to select a valid build to download.\n' % args.os) 'to select a valid build to download.\n' % args.os)
exit(0) exit(0)
else: else:
print '\nBuild %s selected. Downloading #%s...\n' % (lowest_valid_build, answer) print('\n'
'Build %s selected. Downloading #%s...\n'
% (lowest_valid_build, answer))
elif args.auto: elif args.auto:
try: try:
answer answer
except NameError: except NameError:
print ('\n' print('\n'
'No valid version available. ' 'No valid version available. '
'Run again without --auto argument ' 'Run again without --auto argument '
'to select a valid build to download.\n') 'to select a valid build to download.\n')
exit(0) exit(0)
else: else:
print '\nBuild %s selected. Downloading #%s...\n' % (lowest_valid_build, answer) print('\n'
'Build %s selected. Downloading #%s...\n'
% (lowest_valid_build, answer))
else: else:
# default option to interactively offer selection # default option to interactively offer selection
answer = raw_input( answer = get_input(
'\nChoose a product to download (1-%s): ' % len(product_info)) '\nChoose a product to download (1-%s): ' % len(product_info))
try: try:
index = int(answer) - 1 index = int(answer) - 1
if index < 0: if index < 0:
raise ValueError raise ValueError
product_id = product_info.keys()[index] product_id = list(product_info.keys())[index]
except (ValueError, IndexError): except (ValueError, IndexError):
print 'Exiting.' print('Exiting.')
exit(0) exit(0)
# download all the packages for the selected product # download all the packages for the selected product
@ -779,7 +822,7 @@ def main():
os.unlink(sparse_diskimage_path) os.unlink(sparse_diskimage_path)
# make an empty sparseimage and mount it # make an empty sparseimage and mount it
print 'Making empty sparseimage...' print('Making empty sparseimage...')
sparse_diskimage_path = make_sparse_image(volname, sparse_diskimage_path) sparse_diskimage_path = make_sparse_image(volname, sparse_diskimage_path)
mountpoint = mountdmg(sparse_diskimage_path) mountpoint = mountdmg(sparse_diskimage_path)
if mountpoint: if mountpoint:
@ -788,16 +831,18 @@ def main():
product_info[product_id]['DistributionPath'], product_info[product_id]['DistributionPath'],
mountpoint) mountpoint)
if not success: if not success:
print >> sys.stderr, 'Product installation failed.' print('Product installation failed.', file=sys.stderr)
unmountdmg(mountpoint) unmountdmg(mountpoint)
exit(-1) exit(-1)
# add the seeding program xattr to the app if applicable # add the seeding program xattr to the app if applicable
seeding_program = get_seeding_program(args.catalogurl) seeding_program = get_seeding_program(su_catalog_url)
if seeding_program: if seeding_program:
installer_app = find_installer_app(mountpoint) installer_app = find_installer_app(mountpoint)
if installer_app: if installer_app:
print("Adding seeding program %s extended attribute to app"
% seeding_program)
xattr.setxattr(installer_app, 'SeedProgram', seeding_program) xattr.setxattr(installer_app, 'SeedProgram', seeding_program)
print 'Product downloaded and installed to %s' % sparse_diskimage_path print('Product downloaded and installed to %s' % sparse_diskimage_path)
if args.raw: if args.raw:
unmountdmg(mountpoint) unmountdmg(mountpoint)
else: else: