From 7de1ecab2b737db8898a48bac1f955e78578ed2d Mon Sep 17 00:00:00 2001 From: Greg Neagle Date: Thu, 9 Nov 2017 14:12:03 -0800 Subject: [PATCH] First commit --- LICENSE.md | 11 + README.md | 4 + createbootvolfromautonbi.py | 210 ++++++++++++++++++ installinstallmacos.py | 417 ++++++++++++++++++++++++++++++++++++ make_firmwareupdater_pkg.sh | 51 +++++ 5 files changed, 693 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 createbootvolfromautonbi.py create mode 100755 installinstallmacos.py create mode 100755 make_firmwareupdater_pkg.sh diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5ebc1fd --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,11 @@ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this source code except in compliance with the License. +You may obtain a copy of the License at + + https://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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..14dc272 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +macadmin-scripts + +Some scripts that might be of use to macOS admins. Might be related to Munki; +might not. \ No newline at end of file diff --git a/createbootvolfromautonbi.py b/createbootvolfromautonbi.py new file mode 100755 index 0000000..021d4b0 --- /dev/null +++ b/createbootvolfromautonbi.py @@ -0,0 +1,210 @@ +#!/usr/bin/python +# encoding: utf-8 +# +# 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. +'''A tool to make bootable disk volumes from the output of autonbi. Especially +useful to make bootable disks containing Imagr and the 'SIP-ignoring' kernel, +which allows Imagr to run scripts that affect SIP state, set UAKEL options, and +run the `startosinstall` component, all of which might otherwise require network +booting from a NetInstall-style nbi.''' + +import argparse +import os +import plistlib +import subprocess +import sys +import urlparse + + +# dmg helpers +def mountdmg(dmgpath): + """ + Attempts to mount the dmg at dmgpath and returns first mountpoint + """ + mountpoints = [] + dmgname = os.path.basename(dmgpath) + cmd = ['/usr/bin/hdiutil', 'attach', dmgpath, + '-mountRandom', '/tmp', '-nobrowse', '-plist', + '-owners', 'on'] + proc = subprocess.Popen(cmd, bufsize=-1, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (pliststr, err) = proc.communicate() + if proc.returncode: + print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname) + return None + if pliststr: + plist = plistlib.readPlistFromString(pliststr) + for entity in plist['system-entities']: + if 'mount-point' in entity: + mountpoints.append(entity['mount-point']) + + return mountpoints[0] + + +def unmountdmg(mountpoint): + """ + Unmounts the dmg at mountpoint + """ + proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint], + bufsize=-1, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (dummy_output, err) = proc.communicate() + if proc.returncode: + print >> sys.stderr, 'Polite unmount failed: %s' % err + print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint + # try forcing the unmount + retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint, + '-force']) + if retcode: + print >> sys.stderr, 'Failed to unmount %s' % mountpoint + + +def locate_basesystem_dmg(nbi): + '''Finds and returns the relative path to the BaseSystem.dmg within the + NetInstall.dmg''' + source_boot_plist = os.path.join(nbi, 'i386/com.apple.Boot.plist') + try: + boot_args = plistlib.readPlist(source_boot_plist) + except Exception, err: + print >> sys.stderr, err + sys.exit(-1) + kernel_flags = boot_args.get('Kernel Flags') + if not kernel_flags: + print >> sys.stderr, 'i386/com.apple.Boot.plist is missing Kernel Flags' + sys.exit(-1) + # kernel flags should in the form 'root-dmg=file:///path' + if not kernel_flags.startswith('root-dmg='): + print >> sys.stderr, 'Unexpected Kernel Flags: %s' % kernel_flags + sys.exit(-1) + file_url = kernel_flags[9:] + dmg_path = urlparse.unquote(urlparse.urlparse(file_url).path) + # return path minus leading slash + return dmg_path.lstrip('/') + + +def copy_system_version_plist(nbi, target_volume): + '''Copies System/Library/CoreServices/SystemVersion.plist from the + BaseSystem.dmg to the target volume.''' + netinstall_dmg = os.path.join(nbi, 'NetInstall.dmg') + if not os.path.exists(netinstall_dmg): + print >> sys.stderr, "Missing NetInstall.dmg from nbi folder" + sys.exit(-1) + print 'Mounting %s...' % netinstall_dmg + netinstall_mount = mountdmg(netinstall_dmg) + if not netinstall_mount: + sys.exit(-1) + basesystem_dmg = os.path.join(netinstall_mount, locate_basesystem_dmg(nbi)) + print 'Mounting %s...' % basesystem_dmg + basesystem_mount = mountdmg(basesystem_dmg) + if not basesystem_mount: + unmountdmg(netinstall_mount) + sys.exit(-1) + source = os.path.join( + basesystem_mount, 'System/Library/CoreServices/SystemVersion.plist') + dest = os.path.join( + target_volume, 'System/Library/CoreServices/SystemVersion.plist') + try: + subprocess.check_call( + ['/usr/bin/ditto', '-V', source, dest]) + except subprocess.CalledProcessError, err: + print >> sys.stderr, err + unmountdmg(basesystem_mount) + unmountdmg(netinstall_mount) + sys.exit(-1) + + unmountdmg(basesystem_mount) + unmountdmg(netinstall_mount) + + +def copy_boot_files(nbi, target_volume): + '''Copies some boot files, yo''' + files_to_copy = [ + ['NetInstall.dmg', 'NetInstall.dmg'], + ['i386/PlatformSupport.plist', + 'System/Library/CoreServices/PlatformSupport.plist'], + ['i386/booter', 'System/Library/CoreServices/boot.efi'], + ['i386/booter', 'usr/standalone/i386/boot.efi'], + ['i386/x86_64/kernelcache', + 'System/Library/PrelinkedKernels/prelinkedkernel'] + ] + for source, dest in files_to_copy: + full_source = os.path.join(nbi, source) + full_dest = os.path.join(target_volume, dest) + try: + subprocess.check_call( + ['/usr/bin/ditto', '-V', full_source, full_dest]) + except subprocess.CalledProcessError, err: + print >> sys.stderr, err + sys.exit(-1) + + +def make_boot_plist(nbi, target_volume): + '''Creates our com.apple.Boot.plist''' + source_boot_plist = os.path.join(nbi, 'i386/com.apple.Boot.plist') + try: + boot_args = plistlib.readPlist(source_boot_plist) + except Exception, err: + print >> sys.stderr, err + sys.exit(-1) + kernel_flags = boot_args.get('Kernel Flags') + if not kernel_flags: + print >> sys.stderr, 'i386/com.apple.Boot.plist is missing Kernel Flags' + sys.exit(-1) + # prepend the container-dmg path + boot_args['Kernel Flags'] = ( + 'container-dmg=file:///NetInstall.dmg ' + kernel_flags) + boot_plist = os.path.join( + target_volume, + 'Library/Preferences/SystemConfiguration/com.apple.Boot.plist') + plist_dir = os.path.dirname(boot_plist) + if not os.path.exists(plist_dir): + os.makedirs(plist_dir) + try: + plistlib.writePlist(boot_args, boot_plist) + except Exception, err: + print >> sys.stderr, err + sys.exit(-1) + + +def bless(target_volume, label=None): + '''Bless the target volume''' + blessfolder = os.path.join(target_volume, 'System/Library/CoreServices') + if not label: + label = os.path.basename(target_volume) + try: + subprocess.check_call( + ['/usr/sbin/bless', '--folder', blessfolder, '--label', label]) + except subprocess.CalledProcessError, err: + print >> sys.stderr, err + sys.exit(-1) + + +def main(): + '''Do the thing we were made for''' + parser = argparse.ArgumentParser() + parser.add_argument('--nbi', required=True, metavar='path_to_nbi', + help='Path to nbi folder created by autonbi.') + parser.add_argument('--volume', required=True, + metavar='path_to_disk_volume', + help='Path to disk volume.') + args = parser.parse_args() + copy_system_version_plist(args.nbi, args.volume) + copy_boot_files(args.nbi, args.volume) + make_boot_plist(args.nbi, args.volume) + bless(args.volume) + + +if __name__ == '__main__': + main() diff --git a/installinstallmacos.py b/installinstallmacos.py new file mode 100755 index 0000000..b75da14 --- /dev/null +++ b/installinstallmacos.py @@ -0,0 +1,417 @@ +#!/usr/bin/python +# encoding: utf-8 +# +# 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. +# + +'''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''' + + +import argparse +import os +import plistlib +import subprocess +import sys +import urlparse +from xml.dom import minidom +from xml.parsers.expat import ExpatError + + +DEFAULT_SUCATALOG = ( + 'https://swscan.apple.com/content/catalogs/others/' + 'index-10.13seed-10.13-10.12-10.11-10.10-10.9' + '-mountainlion-lion-snowleopard-leopard.merged-1.sucatalog') + + +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', '8g', '-fs', 'HFS+', + '-volname', volume_name, '-type', 'SPARSE', '-plist', output_path] + try: + output = subprocess.check_output(cmd) + except subprocess.CalledProcessError, err: + print >> sys.stderr, err + exit(-1) + try: + return plistlib.readPlistFromString(output)[0] + except IndexError, err: + print >> sys.stderr, 'Unexpected output from hdiutil: %s' % output + exit(-1) + except ExpatError, err: + print >> sys.stderr, 'Malformed output from hdiutil: %s' % output + print >> sys.stderr, err + exit(-1) + + +def make_compressed_dmg(app_path, diskimagepath): + """Returns path to newly-created compressed r/o disk image containing + Install macOS.app""" + + print ('Making read-only compressed disk image containing %s...' + % os.path.basename(app_path)) + cmd = ['/usr/bin/hdiutil', 'create', '-fs', 'HFS+', + '-srcfolder', app_path, diskimagepath] + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError, err: + print >> sys.stderr, err + else: + print 'Disk image created at: %s' % diskimagepath + + +def mountdmg(dmgpath): + """ + Attempts to mount the dmg at dmgpath and returns first mountpoint + """ + mountpoints = [] + dmgname = os.path.basename(dmgpath) + cmd = ['/usr/bin/hdiutil', 'attach', dmgpath, + '-mountRandom', '/tmp', '-nobrowse', '-plist', + '-owners', 'on'] + proc = subprocess.Popen(cmd, bufsize=-1, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + (pliststr, err) = proc.communicate() + if proc.returncode: + print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname) + return None + if pliststr: + plist = plistlib.readPlistFromString(pliststr) + for entity in plist['system-entities']: + if 'mount-point' in entity: + mountpoints.append(entity['mount-point']) + + return mountpoints[0] + + +def unmountdmg(mountpoint): + """ + Unmounts the dmg at mountpoint + """ + proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint], + bufsize=-1, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (dummy_output, err) = proc.communicate() + if proc.returncode: + print >> sys.stderr, 'Polite unmount failed: %s' % err + print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint + # try forcing the unmount + retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint, + '-force']) + if retcode: + print >> sys.stderr, 'Failed to unmount %s' % mountpoint + + +def install_product(dist_path, target_vol): + '''Install a product to a target volume''' + cmd = ['/usr/sbin/installer', '-pkg', dist_path, '-target', target_vol] + try: + subprocess.check_call(cmd) + except subprocess.CalledProcessError, err: + print >> sys.stderr, err + + +class ReplicationError(Exception): + '''A custom error when replication fails''' + pass + + +def replicate_url(full_url, root_dir='/tmp', + show_progress=False, ignore_cache=False): + '''Downloads a URL and stores it in the same relative path on our + filesystem. Returns a path to the replicated file.''' + + path = urlparse.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) + if show_progress: + options = '-fL' + else: + options = '-sfL' + curl_cmd = ['/usr/bin/curl', options, '--create-dirs', + '-o', local_file_path] + if not ignore_cache and os.path.exists(local_file_path): + curl_cmd.extend(['-z', local_file_path]) + curl_cmd.append(full_url) + print "Downloading %s..." % full_url + try: + subprocess.check_call(curl_cmd) + except subprocess.CalledProcessError, 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), err: + print >> sys.stderr, 'Error reading %s: %s' % (filename, err) + 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 + 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, err: + print >> sys.stderr, ( + 'Could not replicate %s: %s' % (url, err)) + return None + except KeyError: + print >> sys.stderr, 'Malformed catalog.' + 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 >> sys.stderr, 'Invalid XML in %s' % filename + return dist_info + except IOError, err: + print >> sys.stderr, 'Error reading %s: %s' % (filename, err) + return dist_info + + auxinfos = dom.getElementsByTagName('auxinfo') + if not auxinfos: + return dist_info + auxinfo = auxinfos[0] + key = None + value = None + for node in auxinfo.childNodes: + 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, err: + print >> sys.stderr, 'Could not replicate %s: %s' % (sucatalog, err) + exit(-1) + try: + catalog = plistlib.readPlist(localcatalogpath) + return catalog + except (OSError, IOError, ExpatError), err: + print >> sys.stderr, ( + 'Error reading %s: %s' % (localcatalogpath, err)) + 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: + product_keys = list(catalog['Products'].keys()) + for product_key in product_keys: + product = catalog['Products'][product_key] + try: + if product['ExtendedMetaInfo'][ + 'InstallAssistantPackageIdentifiers'][ + 'OSInstall'] == 'com.apple.mpkg.OSInstall': + 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) + product_info[product_key] = parse_server_metadata(filename) + product = catalog['Products'][product_key] + product_info[product_key]['PostDate'] = str(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, err: + print >> sys.stderr, 'Could not replicate %s: %s' % (dist_url, err) + dist_info = parse_dist(dist_path) + product_info[product_key]['DistributionPath'] = dist_path + product_info[product_key].update(dist_info) + + return product_info + + +def replicate_product(catalog, product_id, workdir, ignore_cache=False): + '''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) + except ReplicationError, err: + print >> sys.stderr, ( + 'Could not replicate %s: %s' % (package['URL'], err)) + exit(-1) + if 'MetadataURL' in package: + try: + replicate_url(package['MetadataURL'], root_dir=workdir, + ignore_cache=ignore_cache) + except ReplicationError, err: + print >> sys.stderr, ( + 'Could not replicate %s: %s' + % (package['MetadataURL'], err)) + exit(-1) + + +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('--catalogurl', metavar='sucatalog_url', + default=DEFAULT_SUCATALOG, + help='Software Update catalog URL.') + 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('--compress', action='store_true', + help='Output a read-only compressed disk image with ' + 'the Install macOS app at the root.') + parser.add_argument('--ignore-cache', action='store_true', + help='Ignore any previously cached files.') + args = parser.parse_args() + + # 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) + product_info = os_installer_product_info( + catalog, args.workdir, ignore_cache=args.ignore_cache) + + if not product_info: + print >> sys.stderr, ( + 'No macOS installer products found in the sucatalog.') + exit(-1) + + # display a menu of choices (some seed catalogs have multiple installers) + print '%2s %12s %10s %8s %s' % ('#', 'ProductID', 'Version', + 'Build', 'Title') + for index, product_id in enumerate(product_info): + print '%2s %12s %10s %8s %s' % (index+1, + product_id, + product_info[product_id]['version'], + product_info[product_id]['BUILD'], + product_info[product_id]['title']) + + answer = raw_input( + '\nChoose a product to download (1-%s): ' % len(product_info)) + try: + index = int(answer) - 1 + if index < 0: + raise ValueError + product_id = product_info.keys()[index] + except (ValueError, IndexError): + print 'Exiting.' + exit(0) + + # download all the packages for the selected product + replicate_product( + catalog, product_id, args.workdir, ignore_cache=args.ignore_cache) + + # generate a name for the sparseimage + volname = ('Install_macOS_%s-%s' + % (product_info[product_id]['version'], + product_info[product_id]['BUILD'])) + sparse_diskimage_path = os.path.join(args.workdir, volname + '.sparseimage') + if os.path.exists(sparse_diskimage_path): + os.unlink(sparse_diskimage_path) + + # make an empty sparseimage and mount it + print 'Making empty sparseimage...' + sparse_diskimage_path = make_sparse_image(volname, sparse_diskimage_path) + mountpoint = mountdmg(sparse_diskimage_path) + if mountpoint: + # install the product to the mounted sparseimage volume + install_product( + product_info[product_id]['DistributionPath'], + mountpoint) + print 'Product downloaded and installed to %s' % sparse_diskimage_path + if not args.compress: + unmountdmg(mountpoint) + else: + # if --compress option given, create a r/o compressed diskimage + # containing the Install macOS app + compressed_diskimagepath = os.path.join( + args.workdir, volname + '.dmg') + if os.path.exists(compressed_diskimagepath): + os.unlink(compressed_diskimagepath) + applications_dir = os.path.join(mountpoint, 'Applications') + for item in os.listdir(applications_dir): + if item.endswith('.app'): + app_path = os.path.join(applications_dir, item) + make_compressed_dmg(app_path, compressed_diskimagepath) + break + # unmount sparseimage + unmountdmg(mountpoint) + # delete sparseimage since we don't need it any longer + os.unlink(sparse_diskimage_path) + + +if __name__ == '__main__': + main() diff --git a/make_firmwareupdater_pkg.sh b/make_firmwareupdater_pkg.sh new file mode 100755 index 0000000..29dcce0 --- /dev/null +++ b/make_firmwareupdater_pkg.sh @@ -0,0 +1,51 @@ +#!/bin/sh +# Based on investigations and work by Pepijn Bruienne +# Expects a single /Applications/Install macOS High Sierra*.app on disk + +# +# 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. + +IDENTIFIER="com.foo.FirmwareUpdateStandalone" +VERSION=1.0 + +# find the Install macOS High Sierra.app and mount the embedded InstallESD disk image +echo "Mounting High Sierra ESD disk image..." +/usr/bin/hdiutil mount /Applications/Install\ macOS\ High\ Sierra*.app/Contents/SharedSupport/InstallESD.dmg + +# expand the FirmwareUpdate.pkg so we can copy resources from it +echo "Expanding FirmwareUpdate.pkg" +/usr/sbin/pkgutil --expand /Volumes/InstallESD/Packages/FirmwareUpdate.pkg /tmp/FirmwareUpdate + +# we don't need the disk image any more +echo "Ejecting disk image..." +/usr/bin/hdiutil eject /Volumes/InstallESD + +# make a place to stage our pkg resources +/bin/mkdir -p /tmp/FirmwareUpdateStandalone/scripts + +# copy the needed resources +echo "Copying package resources..." +/bin/cp /tmp/FirmwareUpdate/Scripts/postinstall_actions/update /tmp/FirmwareUpdateStandalone/scripts/postinstall +# add an exit 0 at the end of the script +echo "" >> /tmp/FirmwareUpdateStandalone/scripts/postinstall +echo "" >> /tmp/FirmwareUpdateStandalone/scripts/postinstall +echo "exit 0" >> /tmp/FirmwareUpdateStandalone/scripts/postinstall +/bin/cp -R /tmp/FirmwareUpdate/Scripts/Tools /tmp/FirmwareUpdateStandalone/scripts/ + +# build the package +echo "Building standalone package..." +/usr/bin/pkgbuild --nopayload --scripts /tmp/FirmwareUpdateStandalone/scripts --identifier "$IDENTIFIER" --version "$VERSION" /tmp/FirmwareUpdateStandalone/FirmwareUpdateStandalone.pkg + +# clean up +/bin/rm -r /tmp/FirmwareUpdate +/bin/rm -r /tmp/FirmwareUpdateStandalone/scripts \ No newline at end of file