From a8a6f150c8153fa16f7790b115d56015d0dc13d4 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Tue, 5 Apr 2022 10:42:52 -0700 Subject: [PATCH 01/13] Python 3 fix for setting Seed Program xattr. --- installinstallmacos.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/installinstallmacos.py b/installinstallmacos.py index 46fc413..984cda0 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -634,7 +634,8 @@ def main(): 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.encode("UTF-8")) print('Product downloaded and installed to %s' % sparse_diskimage_path) if args.raw: unmountdmg(mountpoint) From 944a01cb06ef494af674e5d1c0f5dfdb15021550 Mon Sep 17 00:00:00 2001 From: scheblein Date: Fri, 7 Jul 2023 14:32:59 -0400 Subject: [PATCH 02/13] Update installinstallmacos.py (#115) added catalog url for Ventura --- installinstallmacos.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/installinstallmacos.py b/installinstallmacos.py index 984cda0..0c5d461 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -66,6 +66,9 @@ '21': 'https://swscan.apple.com/content/catalogs/others/' 'index-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', + '22': 'https://swscan.apple.com/content/catalogs/others/' + 'index-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' + '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog' } SEED_CATALOGS_PLIST = ( From 1fc3224198d70d8ed0424544f5d59595a5a6b671 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Tue, 10 Oct 2023 11:34:57 -0700 Subject: [PATCH 03/13] Add default catalog URL for Sonoma --- installinstallmacos.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/installinstallmacos.py b/installinstallmacos.py index 0c5d461..fa0ff7a 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -68,6 +68,9 @@ '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', '22': 'https://swscan.apple.com/content/catalogs/others/' 'index-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' + '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', + '23': 'https://swscan.apple.com/content/catalogs/others/' + 'index-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog' } From ba1eeab9d06a866d19c7b5d5f4656b74f9b5e36f Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Tue, 18 Jun 2024 13:42:17 -0700 Subject: [PATCH 04/13] Add munki_bundle_pkg_finder script --- munki_bundle_pkg_finder.py | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100755 munki_bundle_pkg_finder.py diff --git a/munki_bundle_pkg_finder.py b/munki_bundle_pkg_finder.py new file mode 100755 index 0000000..3538774 --- /dev/null +++ b/munki_bundle_pkg_finder.py @@ -0,0 +1,62 @@ +#!/usr/local/munki/munki-python + +import os +import plistlib +import sys + +sys.path.append("/usr/local/munki") + +from munkilib import dmgutils +from munkilib import pkgutils + +if len(sys.argv) != 2: + print('Need exactly one parameter: path to a munki repo!', file=sys.stderr) + sys.exit(-1) + +repo_path = sys.argv[1] + +all_catalog = os.path.join(repo_path, "catalogs/all") + +with open(all_catalog, mode="rb") as FILE: + all_items = plistlib.load(FILE) + +dmg_items = [{"name": item["name"], + "version": item["version"], + "location": item["installer_item_location"], + "package_path": item.get("package_path", "")} + for item in all_items + if item.get("installer_item_location", "").endswith(".dmg") and + item.get("installer_type") is None] + +items_with_bundle_style_pkgs = [] +for item in dmg_items: + full_path = os.path.join(repo_path, "pkgs", item["location"]) + print("Checking %s..." % full_path) + mountpoints = dmgutils.mountdmg(full_path) + if mountpoints: + pkg_path = item["package_path"] + if pkg_path: + itempath = os.path.join(mountpoints[0], pkg_path) + if os.path.isdir(itempath): + print("***** %s--%s has a bundle-style pkg" + % (item["name"], item["version"])) + items_with_bundle_style_pkgs.append(item) + else: + for file_item in os.listdir(mountpoints[0]): + if pkgutils.hasValidInstallerItemExt(file_item): + itempath = os.path.join(mountpoints[0], file_item) + if os.path.isdir(itempath): + print("***** %s--%s has a bundle-style pkg" + % (item["name"], item["version"])) + items_with_bundle_style_pkgs.append(item) + break + dmgutils.unmountdmg(mountpoints[0]) + else: + print("No filesystems mounted from %s" % full_path) + continue + +print("Found %s items with bundle-style pkgs." + % len(items_with_bundle_style_pkgs)) +for item in sorted(items_with_bundle_style_pkgs, key=lambda d: d["name"]): + print("%s--%s"% (item["name"], item["version"])) + print(" %s" % item["location"]) From 77e507e4351bd35db771706c4f3d6955b6bce968 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 18 Sep 2024 08:45:38 -0700 Subject: [PATCH 05/13] Add Sequoia sucatalog URL --- installinstallmacos.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/installinstallmacos.py b/installinstallmacos.py index fa0ff7a..7ac8d44 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -71,6 +71,9 @@ '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', '23': 'https://swscan.apple.com/content/catalogs/others/' 'index-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' + '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', + '24': 'https://swscan.apple.com/content/catalogs/others/' + 'index-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog' } From 85c643e68416c5bca9622baf007f51b980b6d7f4 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Tue, 24 Jun 2025 09:52:55 -0700 Subject: [PATCH 06/13] macOS 26 updates --- installinstallmacos.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/installinstallmacos.py b/installinstallmacos.py index 7ac8d44..c757202 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -74,6 +74,9 @@ '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', '24': 'https://swscan.apple.com/content/catalogs/others/' 'index-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' + '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog', + '25': 'https://swscan.apple.com/content/catalogs/others/' + 'index-26-15-14-13-12-10.16-10.15-10.14-10.13-10.12-10.11-10.10-10.9' '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog' } @@ -152,7 +155,8 @@ def get_default_catalog(): def make_sparse_image(volume_name, output_path): '''Make a sparse disk image we can install a product to''' - cmd = ['/usr/bin/hdiutil', 'create', '-size', '16g', '-fs', 'HFS+', + # note: for macOS 26 Tahoe we needed to increase the size + cmd = ['/usr/bin/hdiutil', 'create', '-size', '20g', '-fs', 'HFS+', '-volname', volume_name, '-type', 'SPARSE', '-plist', output_path] try: output = subprocess.check_output(cmd) From a66413ec7e6112b4873048bf0fec332a3cab0cfd Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Sat, 12 Jul 2025 06:27:32 -0700 Subject: [PATCH 07/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7759c22..a38f77e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ might not. These are currently only supported using Apple's Python on macOS. There is no support for running these on Windows or Linux. -In macOS 12.3, Apple will be removing its Python 2.7 install. You'll need to provide your own Python to use these scripts. You may also need to install additional Python modules. +In macOS 12.3, Apple stopped providung Python as part of macOS. You'll need to provide your own Python to use these scripts. You may also need to install additional Python modules. #### getmacosipsws.py From 2cc8b899204838cfcf4d8c162a75182b3e32daab Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Sat, 12 Jul 2025 06:28:47 -0700 Subject: [PATCH 08/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a38f77e..f98b1d3 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Some scripts that might be of use to macOS admins. Might be related to Munki; might not. -These are currently only supported using Apple's Python on macOS. There is no support for running these on Windows or Linux. +These are only supported on macOS. There is no support for running these on Windows or Linux. In macOS 12.3, Apple stopped providung Python as part of macOS. You'll need to provide your own Python to use these scripts. You may also need to install additional Python modules. From 522fc7ff40fbd17839bf4714e103bc52fa232f59 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Wed, 20 Aug 2025 13:55:28 -0700 Subject: [PATCH 09/13] Workaround for issue with installer in macOS 15.6 --- installinstallmacos.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/installinstallmacos.py b/installinstallmacos.py index c757202..3b70da7 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -239,7 +239,12 @@ def install_product(dist_path, target_vol): # set CM_BUILD env var to make Installer bypass eligibilty checks # when installing packages (for machine-specific OS builds) os.environ["CM_BUILD"] = "CM_BUILD" - cmd = ['/usr/sbin/installer', '-pkg', dist_path, '-target', target_vol] + #cmd = ['/usr/sbin/installer', '-pkg', dist_path, '-target', target_vol] + # a hack to work around a change in macOS 15.6+ since installing a .dist + # file no longer works + dist_dir = os.path.dirname(dist_path) + install_asst_pkg = os.path.join(dist_dir, "InstallAssistant.pkg") + cmd = ['/usr/sbin/installer', '-pkg', install_asst_pkg, '-target', target_vol] try: subprocess.check_call(cmd) except subprocess.CalledProcessError as err: From efe68b89a9dca86a721cf3de49c015c309a20e01 Mon Sep 17 00:00:00 2001 From: Ubihazard <98227985+ubihazard@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:20:57 +0300 Subject: [PATCH 10/13] Bring back ability to make installer images for Catalina and older after commit 522fc7f (#133) * Fix for installer workaround introduced in commit 522fc7f no longer being able to make images for High Sierra, Mojave, and Catalina Authored-by: ubihazard --- installinstallmacos.py | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/installinstallmacos.py b/installinstallmacos.py index 3b70da7..d7a513a 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -33,6 +33,7 @@ import plistlib import subprocess import sys +import platform try: # python 2 from urllib.parse import urlsplit @@ -233,18 +234,40 @@ def unmountdmg(mountpoint): print('Failed to unmount %s' % mountpoint, file=sys.stderr) -def install_product(dist_path, target_vol): +def check_is_legacy(title): + if "Sierra" in title or "Mojave" in title or "Catalina" in title: + return True + return False + + +def install_product(dist_path, target_vol, is_legacy): '''Install a product to a target volume. Returns a boolean to indicate success or failure.''' # set CM_BUILD env var to make Installer bypass eligibilty checks # when installing packages (for machine-specific OS builds) os.environ["CM_BUILD"] = "CM_BUILD" - #cmd = ['/usr/sbin/installer', '-pkg', dist_path, '-target', target_vol] - # a hack to work around a change in macOS 15.6+ since installing a .dist - # file no longer works - dist_dir = os.path.dirname(dist_path) - install_asst_pkg = os.path.join(dist_dir, "InstallAssistant.pkg") - cmd = ['/usr/sbin/installer', '-pkg', install_asst_pkg, '-target', target_vol] + # check if running on Sequoia 15.6+ + is_15_6_or_later = False + os_ver = platform.mac_ver()[0].split('.') + ver_major = int(os_ver[0]) + ver_minor = int(os_ver[1]) + if ver_major >= 15: + if ver_major > 15 or ver_minor >= 6: + is_15_6_or_later = True + if is_legacy: + if is_15_6_or_later: + print('*** Error: building High Sierra, Mojave, and Catalina installer images') + print('*** is unsupported on macOS Sequoia 15.6 and later due to breaking changes') + print('*** in /usr/sbin/installer by Apple.') + return False + else: + cmd = ['/usr/sbin/installer', '-pkg', dist_path, '-target', target_vol] + else: + # a hack to work around a change in macOS 15.6+ since installing a .dist + # file no longer works + dist_dir = os.path.dirname(dist_path) + install_asst_pkg = os.path.join(dist_dir, "InstallAssistant.pkg") + cmd = ['/usr/sbin/installer', '-pkg', install_asst_pkg, '-target', target_vol] try: subprocess.check_call(cmd) except subprocess.CalledProcessError as err: @@ -487,7 +510,7 @@ def os_installer_product_info(catalog, workdir, ignore_cache=False): 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 @@ -640,7 +663,8 @@ def main(): # install the product to the mounted sparseimage volume success = install_product( product_info[product_id]['DistributionPath'], - mountpoint) + mountpoint, + check_is_legacy(product_info[product_id]['title'])) if not success: print('Product installation failed.', file=sys.stderr) unmountdmg(mountpoint) From 40ae77b9c66830a8094796da9b7a41d34513681a Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Thu, 30 Oct 2025 09:00:27 -0700 Subject: [PATCH 11/13] Simplify logic when running on pre-macOS 15.6 and when dealing with pre-Big Sur installers --- installinstallmacos.py | 67 +++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/installinstallmacos.py b/installinstallmacos.py index d7a513a..502c7f7 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -34,6 +34,7 @@ import subprocess import sys import platform + try: # python 2 from urllib.parse import urlsplit @@ -234,40 +235,44 @@ def unmountdmg(mountpoint): print('Failed to unmount %s' % mountpoint, file=sys.stderr) -def check_is_legacy(title): - if "Sierra" in title or "Mojave" in title or "Catalina" in title: - return True - return False +def is_legacy(title): + """Returns a boolean to tell us if this is a pre-Big Sur installer""" + return "Sierra" in title or "Mojave" in title or "Catalina" in title + + +def is_15_6_or_later(): + """Returns a boolean to indicated whether we're running on macOS 15.6 or later""" + os_ver = platform.mac_ver()[0].split('.') + ver_major = int(os_ver[0]) + ver_minor = int(os_ver[1]) + if ver_major >= 15: + if ver_major > 15 or ver_minor >= 6: + return True + return False -def install_product(dist_path, target_vol, is_legacy): +def install_product(dist_path, target_vol): '''Install a product to a target volume. Returns a boolean to indicate success or failure.''' # set CM_BUILD env var to make Installer bypass eligibilty checks # when installing packages (for machine-specific OS builds) os.environ["CM_BUILD"] = "CM_BUILD" # check if running on Sequoia 15.6+ - is_15_6_or_later = False - os_ver = platform.mac_ver()[0].split('.') - ver_major = int(os_ver[0]) - ver_minor = int(os_ver[1]) - if ver_major >= 15: - if ver_major > 15 or ver_minor >= 6: - is_15_6_or_later = True - if is_legacy: - if is_15_6_or_later: - print('*** Error: building High Sierra, Mojave, and Catalina installer images') - print('*** is unsupported on macOS Sequoia 15.6 and later due to breaking changes') - print('*** in /usr/sbin/installer by Apple.') - return False - else: - cmd = ['/usr/sbin/installer', '-pkg', dist_path, '-target', target_vol] + if is_15_6_or_later(): + # work around a change in macOS 15.6+ since installing a .dist + # file no longer works + # find InstallAssistant.pkg and install that instead + dist_dir = os.path.dirname(dist_path) + install_asst_pkg = os.path.join(dist_dir, "InstallAssistant.pkg") + if not os.path.exists(install_asst_pkg): + print("*** Error: InstallAssistant.pkg not found.", file=sys.stderr) + return False + cmd = ['/usr/sbin/installer', '-pkg', install_asst_pkg, '-target', target_vol] else: - # a hack to work around a change in macOS 15.6+ since installing a .dist - # file no longer works - dist_dir = os.path.dirname(dist_path) - install_asst_pkg = os.path.join(dist_dir, "InstallAssistant.pkg") - cmd = ['/usr/sbin/installer', '-pkg', install_asst_pkg, '-target', target_vol] + # pre-macOS 15.6 install method + # (install the .dist, which acts like installing a distribution pkg) + cmd = ['/usr/sbin/installer', '-pkg', dist_path, '-target', target_vol] + try: subprocess.check_call(cmd) except subprocess.CalledProcessError as err: @@ -642,6 +647,15 @@ def main(): except (ValueError, IndexError): print('Exiting.') exit(0) + + if is_legacy(product_info[product_id].get('title','')): + # Catalina and earlier do not have InstallAssistant.pkg, and + # InstallAssistantAuto.pkg (which they do have) do not work for + # installing the Install macOS.app + print('*** Error: building High Sierra, Mojave, and Catalina installer images', file=sys.stderr) + print('*** is unsupported on macOS Sequoia 15.6 and later due to breaking changes', file=sys.stderr) + print('*** in /usr/sbin/installer by Apple.', file=sys.stderr) + exit(1) # download all the packages for the selected product replicate_product( @@ -663,8 +677,7 @@ def main(): # install the product to the mounted sparseimage volume success = install_product( product_info[product_id]['DistributionPath'], - mountpoint, - check_is_legacy(product_info[product_id]['title'])) + mountpoint) if not success: print('Product installation failed.', file=sys.stderr) unmountdmg(mountpoint) From 1275ba4af3428f6612f1830ec4caf8c8486e2f04 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Thu, 30 Oct 2025 09:26:54 -0700 Subject: [PATCH 12/13] Several pylint cleanups --- installinstallmacos.py | 106 +++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/installinstallmacos.py b/installinstallmacos.py index 502c7f7..0a135b1 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -44,6 +44,9 @@ from xml.dom import minidom from xml.parsers.expat import ExpatError +# disable pylint's suggestions about using f-strings +# pylint: disable=C0209 + try: import xattr except ImportError: @@ -104,7 +107,9 @@ def read_plist(filepath): return plistlib.load(fileobj) except AttributeError: # plistlib module doesn't have a load function (as in Python 2) + # pylint: disable=E1101 return plistlib.readPlist(filepath) + # pylint: enable=E1101 def read_plist_from_string(bytestring): @@ -113,7 +118,9 @@ def read_plist_from_string(bytestring): return plistlib.loads(bytestring) except AttributeError: # plistlib module doesn't have a load function (as in Python 2) + # pylint: disable=E1101 return plistlib.readPlistFromString(bytestring) + # pylint: enable=E1101 def get_seeding_program(sucatalog_url): @@ -164,16 +171,17 @@ def make_sparse_image(volume_name, output_path): output = subprocess.check_output(cmd) except subprocess.CalledProcessError as err: print(err, file=sys.stderr) - exit(-1) + sys.exit(-1) try: - return read_plist_from_string(output)[0] - except IndexError as err: + output = read_plist_from_string(output)[0] + except IndexError: print('Unexpected output from hdiutil: %s' % output, file=sys.stderr) - exit(-1) + sys.exit(-1) except ExpatError as err: print('Malformed output from hdiutil: %s' % output, file=sys.stderr) print(err, file=sys.stderr) - exit(-1) + sys.exit(-1) + return output def make_compressed_dmg(app_path, diskimagepath): @@ -265,8 +273,8 @@ def install_product(dist_path, target_vol): dist_dir = os.path.dirname(dist_path) install_asst_pkg = os.path.join(dist_dir, "InstallAssistant.pkg") if not os.path.exists(install_asst_pkg): - print("*** Error: InstallAssistant.pkg not found.", file=sys.stderr) - return False + print("*** Error: InstallAssistant.pkg not found.", file=sys.stderr) + return False cmd = ['/usr/sbin/installer', '-pkg', install_asst_pkg, '-target', target_vol] else: # pre-macOS 15.6 install method @@ -278,29 +286,30 @@ def install_product(dist_path, target_vol): except subprocess.CalledProcessError as err: print(err, file=sys.stderr) return False - else: - # Apple postinstall script bug ends up copying files to a path like - # /tmp/dmg.T9ak1HApplications - path = target_vol + 'Applications' - if os.path.exists(path): - print('*********************************************************') - print('*** Working around a very dumb Apple bug in a package ***') - print('*** postinstall script that fails to correctly target ***') - print('*** the Install macOS.app when installed to a volume ***') - print('*** other than the current boot volume. ***') - print('*** Please file feedback with Apple! ***') - print('*********************************************************') - subprocess.check_call( - ['/usr/bin/ditto', - path, - os.path.join(target_vol, 'Applications')] - ) - subprocess.check_call(['/bin/rm', '-r', path]) - return True + + # Apple postinstall script bug ends up copying files to a path like + # /tmp/dmg.T9ak1HApplications + path = target_vol + 'Applications' + if os.path.exists(path): + print('*********************************************************') + print('*** Working around a very dumb Apple bug in a package ***') + print('*** postinstall script that fails to correctly target ***') + print('*** the Install macOS.app when installed to a volume ***') + print('*** other than the current boot volume. ***') + print('*** Please file feedback with Apple! ***') + print('*********************************************************') + subprocess.check_call( + ['/usr/bin/ditto', + path, + os.path.join(target_vol, 'Applications')] + ) + subprocess.check_call(['/bin/rm', '-r', path]) + return True + class ReplicationError(Exception): '''A custom error when replication fails''' - pass + #pass def replicate_url(full_url, @@ -340,7 +349,7 @@ def replicate_url(full_url, print("Downloading %s..." % full_url) need_download = False try: - output = subprocess.check_output(curl_cmd) + _ = subprocess.check_output(curl_cmd) except subprocess.CalledProcessError as err: if not resumed or not err.output.isdigit(): raise ReplicationError(err) @@ -446,7 +455,7 @@ def download_and_parse_sucatalog(sucatalog, workdir, ignore_cache=False): 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) + sys.exit(-1) if os.path.splitext(localcatalogpath)[1] == '.gz': with gzip.open(localcatalogpath) as the_file: content = the_file.read() @@ -456,7 +465,7 @@ def download_and_parse_sucatalog(sucatalog, workdir, ignore_cache=False): except ExpatError as err: print('Error reading %s: %s' % (localcatalogpath, err), file=sys.stderr) - exit(-1) + sys.exit(-1) else: try: catalog = read_plist(localcatalogpath) @@ -464,7 +473,7 @@ def download_and_parse_sucatalog(sucatalog, workdir, ignore_cache=False): except (OSError, IOError, ExpatError) as err: print('Error reading %s: %s' % (localcatalogpath, err), file=sys.stderr) - exit(-1) + sys.exit(-1) def find_mac_os_installers(catalog): @@ -529,13 +538,16 @@ def replicate_product(catalog, product_id, workdir, ignore_cache=False): if 'URL' in package: try: replicate_url( - package['URL'], root_dir=workdir, - show_progress=True, ignore_cache=ignore_cache, - attempt_resume=(not ignore_cache)) + package['URL'], + root_dir=workdir, + show_progress=True, + ignore_cache=ignore_cache, + attempt_resume=not ignore_cache + ) except ReplicationError as err: print('Could not replicate %s: %s' % (package['URL'], err), file=sys.stderr) - exit(-1) + sys.exit(-1) if 'MetadataURL' in package: try: replicate_url(package['MetadataURL'], root_dir=workdir, @@ -543,7 +555,7 @@ def replicate_product(catalog, product_id, workdir, ignore_cache=False): except ReplicationError as err: print('Could not replicate %s: %s' % (package['MetadataURL'], err), file=sys.stderr) - exit(-1) + sys.exit(-1) def find_installer_app(mountpoint): @@ -605,13 +617,13 @@ def main(): % args.seedprogram, file=sys.stderr) print('Valid seeding programs are: %s' % ', '.join(get_seeding_programs()), file=sys.stderr) - exit(-1) + sys.exit(-1) else: 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) + sys.exit(-1) # download sucatalog and look for products that are for macOS installers catalog = download_and_parse_sucatalog( @@ -622,7 +634,7 @@ def main(): if not product_info: print('No macOS installer products found in the sucatalog.', file=sys.stderr) - exit(-1) + sys.exit(-1) # display a menu of choices (some seed catalogs have multiple installers) print('%2s %14s %10s %8s %11s %s' @@ -646,16 +658,18 @@ def main(): product_id = list(product_info.keys())[index] except (ValueError, IndexError): print('Exiting.') - exit(0) - + sys.exit(0) + if is_legacy(product_info[product_id].get('title','')): # Catalina and earlier do not have InstallAssistant.pkg, and # InstallAssistantAuto.pkg (which they do have) do not work for # installing the Install macOS.app - print('*** Error: building High Sierra, Mojave, and Catalina installer images', file=sys.stderr) - print('*** is unsupported on macOS Sequoia 15.6 and later due to breaking changes', file=sys.stderr) - print('*** in /usr/sbin/installer by Apple.', file=sys.stderr) - exit(1) + print( + '*** Error: building High Sierra, Mojave, and Catalina installer images\n' + '*** is unsupported on macOS Sequoia 15.6 and later due to breaking changes\n' + '*** in /usr/sbin/installer by Apple.', file=sys.stderr + ) + sys.exit(1) # download all the packages for the selected product replicate_product( @@ -681,7 +695,7 @@ def main(): if not success: print('Product installation failed.', file=sys.stderr) unmountdmg(mountpoint) - exit(-1) + sys.exit(-1) # add the seeding program xattr to the app if applicable seeding_program = get_seeding_program(su_catalog_url) if seeding_program: From 8ceb10ffef2c43c2c1e1421eed97fbca3c091c23 Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Tue, 4 Nov 2025 09:09:49 -0800 Subject: [PATCH 13/13] Update how we check macOS version to deal with older versions of Python compiled against pre-macOS 11 SDK --- installinstallmacos.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/installinstallmacos.py b/installinstallmacos.py index 0a135b1..4932c43 100755 --- a/installinstallmacos.py +++ b/installinstallmacos.py @@ -248,15 +248,28 @@ def is_legacy(title): return "Sierra" in title or "Mojave" in title or "Catalina" in title -def is_15_6_or_later(): - """Returns a boolean to indicated whether we're running on macOS 15.6 or later""" - os_ver = platform.mac_ver()[0].split('.') - ver_major = int(os_ver[0]) - ver_minor = int(os_ver[1]) - if ver_major >= 15: - if ver_major > 15 or ver_minor >= 6: - return True - return False +def macOsVersion(only_major_minor=True): + """Returns a macOS version as a tuple of integers + + Args: + only_major_minor: Boolean. If True, only include major/minor versions. + """ + # platform.mac_ver() returns 10.16-style version info on Big Sur + # and is likely to do so until Python is compiled with the macOS 11 SDK + # which may not happen for a while. And Apple's odd tricks mean that even + # reading /System/Library/CoreServices/SystemVersion.plist is unreliable. + # So let's use a different method. + try: + os_version_tuple = subprocess.check_output( + ('/usr/bin/sw_vers', '-productVersion'), + env={'SYSTEM_VERSION_COMPAT': '0'} + ).decode('UTF-8').rstrip().split('.') + except subprocess.CalledProcessError: + # fall back to platform.mac_ver() + os_version_tuple = platform.mac_ver()[0].split(".") + if only_major_minor: + os_version_tuple = os_version_tuple[0:2] + return tuple(map(int, os_version_tuple)) def install_product(dist_path, target_vol): @@ -266,7 +279,7 @@ def install_product(dist_path, target_vol): # when installing packages (for machine-specific OS builds) os.environ["CM_BUILD"] = "CM_BUILD" # check if running on Sequoia 15.6+ - if is_15_6_or_later(): + if macOsVersion() >= (15,6): # work around a change in macOS 15.6+ since installing a .dist # file no longer works # find InstallAssistant.pkg and install that instead