|
| 1 | +#!/usr/bin/python |
| 2 | +# encoding: utf-8 |
| 3 | +# |
| 4 | +# Copyright 2017 Greg Neagle. |
| 5 | +# |
| 6 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | +# you may not use this file except in compliance with the License. |
| 8 | +# You may obtain a copy of the License at |
| 9 | +# |
| 10 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +# |
| 12 | +# Unless required by applicable law or agreed to in writing, software |
| 13 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | +# See the License for the specific language governing permissions and |
| 16 | +# limitations under the License. |
| 17 | +'''A tool to make bootable disk volumes from the output of autonbi. Especially |
| 18 | +useful to make bootable disks containing Imagr and the 'SIP-ignoring' kernel, |
| 19 | +which allows Imagr to run scripts that affect SIP state, set UAKEL options, and |
| 20 | +run the `startosinstall` component, all of which might otherwise require network |
| 21 | +booting from a NetInstall-style nbi.''' |
| 22 | + |
| 23 | +import argparse |
| 24 | +import os |
| 25 | +import plistlib |
| 26 | +import subprocess |
| 27 | +import sys |
| 28 | +import urlparse |
| 29 | + |
| 30 | + |
| 31 | +# dmg helpers |
| 32 | +def mountdmg(dmgpath): |
| 33 | + """ |
| 34 | + Attempts to mount the dmg at dmgpath and returns first mountpoint |
| 35 | + """ |
| 36 | + mountpoints = [] |
| 37 | + dmgname = os.path.basename(dmgpath) |
| 38 | + cmd = ['/usr/bin/hdiutil', 'attach', dmgpath, |
| 39 | + '-mountRandom', '/tmp', '-nobrowse', '-plist', |
| 40 | + '-owners', 'on'] |
| 41 | + proc = subprocess.Popen(cmd, bufsize=-1, |
| 42 | + stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 43 | + (pliststr, err) = proc.communicate() |
| 44 | + if proc.returncode: |
| 45 | + print >> sys.stderr, 'Error: "%s" while mounting %s.' % (err, dmgname) |
| 46 | + return None |
| 47 | + if pliststr: |
| 48 | + plist = plistlib.readPlistFromString(pliststr) |
| 49 | + for entity in plist['system-entities']: |
| 50 | + if 'mount-point' in entity: |
| 51 | + mountpoints.append(entity['mount-point']) |
| 52 | + |
| 53 | + return mountpoints[0] |
| 54 | + |
| 55 | + |
| 56 | +def unmountdmg(mountpoint): |
| 57 | + """ |
| 58 | + Unmounts the dmg at mountpoint |
| 59 | + """ |
| 60 | + proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint], |
| 61 | + bufsize=-1, stdout=subprocess.PIPE, |
| 62 | + stderr=subprocess.PIPE) |
| 63 | + (dummy_output, err) = proc.communicate() |
| 64 | + if proc.returncode: |
| 65 | + print >> sys.stderr, 'Polite unmount failed: %s' % err |
| 66 | + print >> sys.stderr, 'Attempting to force unmount %s' % mountpoint |
| 67 | + # try forcing the unmount |
| 68 | + retcode = subprocess.call(['/usr/bin/hdiutil', 'detach', mountpoint, |
| 69 | + '-force']) |
| 70 | + if retcode: |
| 71 | + print >> sys.stderr, 'Failed to unmount %s' % mountpoint |
| 72 | + |
| 73 | + |
| 74 | +def locate_basesystem_dmg(nbi): |
| 75 | + '''Finds and returns the relative path to the BaseSystem.dmg within the |
| 76 | + NetInstall.dmg''' |
| 77 | + source_boot_plist = os.path.join(nbi, 'i386/com.apple.Boot.plist') |
| 78 | + try: |
| 79 | + boot_args = plistlib.readPlist(source_boot_plist) |
| 80 | + except Exception, err: |
| 81 | + print >> sys.stderr, err |
| 82 | + sys.exit(-1) |
| 83 | + kernel_flags = boot_args.get('Kernel Flags') |
| 84 | + if not kernel_flags: |
| 85 | + print >> sys.stderr, 'i386/com.apple.Boot.plist is missing Kernel Flags' |
| 86 | + sys.exit(-1) |
| 87 | + # kernel flags should in the form 'root-dmg=file:///path' |
| 88 | + if not kernel_flags.startswith('root-dmg='): |
| 89 | + print >> sys.stderr, 'Unexpected Kernel Flags: %s' % kernel_flags |
| 90 | + sys.exit(-1) |
| 91 | + file_url = kernel_flags[9:] |
| 92 | + dmg_path = urlparse.unquote(urlparse.urlparse(file_url).path) |
| 93 | + # return path minus leading slash |
| 94 | + return dmg_path.lstrip('/') |
| 95 | + |
| 96 | + |
| 97 | +def copy_system_version_plist(nbi, target_volume): |
| 98 | + '''Copies System/Library/CoreServices/SystemVersion.plist from the |
| 99 | + BaseSystem.dmg to the target volume.''' |
| 100 | + netinstall_dmg = os.path.join(nbi, 'NetInstall.dmg') |
| 101 | + if not os.path.exists(netinstall_dmg): |
| 102 | + print >> sys.stderr, "Missing NetInstall.dmg from nbi folder" |
| 103 | + sys.exit(-1) |
| 104 | + print 'Mounting %s...' % netinstall_dmg |
| 105 | + netinstall_mount = mountdmg(netinstall_dmg) |
| 106 | + if not netinstall_mount: |
| 107 | + sys.exit(-1) |
| 108 | + basesystem_dmg = os.path.join(netinstall_mount, locate_basesystem_dmg(nbi)) |
| 109 | + print 'Mounting %s...' % basesystem_dmg |
| 110 | + basesystem_mount = mountdmg(basesystem_dmg) |
| 111 | + if not basesystem_mount: |
| 112 | + unmountdmg(netinstall_mount) |
| 113 | + sys.exit(-1) |
| 114 | + source = os.path.join( |
| 115 | + basesystem_mount, 'System/Library/CoreServices/SystemVersion.plist') |
| 116 | + dest = os.path.join( |
| 117 | + target_volume, 'System/Library/CoreServices/SystemVersion.plist') |
| 118 | + try: |
| 119 | + subprocess.check_call( |
| 120 | + ['/usr/bin/ditto', '-V', source, dest]) |
| 121 | + except subprocess.CalledProcessError, err: |
| 122 | + print >> sys.stderr, err |
| 123 | + unmountdmg(basesystem_mount) |
| 124 | + unmountdmg(netinstall_mount) |
| 125 | + sys.exit(-1) |
| 126 | + |
| 127 | + unmountdmg(basesystem_mount) |
| 128 | + unmountdmg(netinstall_mount) |
| 129 | + |
| 130 | + |
| 131 | +def copy_boot_files(nbi, target_volume): |
| 132 | + '''Copies some boot files, yo''' |
| 133 | + files_to_copy = [ |
| 134 | + ['NetInstall.dmg', 'NetInstall.dmg'], |
| 135 | + ['i386/PlatformSupport.plist', |
| 136 | + 'System/Library/CoreServices/PlatformSupport.plist'], |
| 137 | + ['i386/booter', 'System/Library/CoreServices/boot.efi'], |
| 138 | + ['i386/booter', 'usr/standalone/i386/boot.efi'], |
| 139 | + ['i386/x86_64/kernelcache', |
| 140 | + 'System/Library/PrelinkedKernels/prelinkedkernel'] |
| 141 | + ] |
| 142 | + for source, dest in files_to_copy: |
| 143 | + full_source = os.path.join(nbi, source) |
| 144 | + full_dest = os.path.join(target_volume, dest) |
| 145 | + try: |
| 146 | + subprocess.check_call( |
| 147 | + ['/usr/bin/ditto', '-V', full_source, full_dest]) |
| 148 | + except subprocess.CalledProcessError, err: |
| 149 | + print >> sys.stderr, err |
| 150 | + sys.exit(-1) |
| 151 | + |
| 152 | + |
| 153 | +def make_boot_plist(nbi, target_volume): |
| 154 | + '''Creates our com.apple.Boot.plist''' |
| 155 | + source_boot_plist = os.path.join(nbi, 'i386/com.apple.Boot.plist') |
| 156 | + try: |
| 157 | + boot_args = plistlib.readPlist(source_boot_plist) |
| 158 | + except Exception, err: |
| 159 | + print >> sys.stderr, err |
| 160 | + sys.exit(-1) |
| 161 | + kernel_flags = boot_args.get('Kernel Flags') |
| 162 | + if not kernel_flags: |
| 163 | + print >> sys.stderr, 'i386/com.apple.Boot.plist is missing Kernel Flags' |
| 164 | + sys.exit(-1) |
| 165 | + # prepend the container-dmg path |
| 166 | + boot_args['Kernel Flags'] = ( |
| 167 | + 'container-dmg=file:///NetInstall.dmg ' + kernel_flags) |
| 168 | + boot_plist = os.path.join( |
| 169 | + target_volume, |
| 170 | + 'Library/Preferences/SystemConfiguration/com.apple.Boot.plist') |
| 171 | + plist_dir = os.path.dirname(boot_plist) |
| 172 | + if not os.path.exists(plist_dir): |
| 173 | + os.makedirs(plist_dir) |
| 174 | + try: |
| 175 | + plistlib.writePlist(boot_args, boot_plist) |
| 176 | + except Exception, err: |
| 177 | + print >> sys.stderr, err |
| 178 | + sys.exit(-1) |
| 179 | + |
| 180 | + |
| 181 | +def bless(target_volume, label=None): |
| 182 | + '''Bless the target volume''' |
| 183 | + blessfolder = os.path.join(target_volume, 'System/Library/CoreServices') |
| 184 | + if not label: |
| 185 | + label = os.path.basename(target_volume) |
| 186 | + try: |
| 187 | + subprocess.check_call( |
| 188 | + ['/usr/sbin/bless', '--folder', blessfolder, '--label', label]) |
| 189 | + except subprocess.CalledProcessError, err: |
| 190 | + print >> sys.stderr, err |
| 191 | + sys.exit(-1) |
| 192 | + |
| 193 | + |
| 194 | +def main(): |
| 195 | + '''Do the thing we were made for''' |
| 196 | + parser = argparse.ArgumentParser() |
| 197 | + parser.add_argument('--nbi', required=True, metavar='path_to_nbi', |
| 198 | + help='Path to nbi folder created by autonbi.') |
| 199 | + parser.add_argument('--volume', required=True, |
| 200 | + metavar='path_to_disk_volume', |
| 201 | + help='Path to disk volume.') |
| 202 | + args = parser.parse_args() |
| 203 | + copy_system_version_plist(args.nbi, args.volume) |
| 204 | + copy_boot_files(args.nbi, args.volume) |
| 205 | + make_boot_plist(args.nbi, args.volume) |
| 206 | + bless(args.volume) |
| 207 | + |
| 208 | + |
| 209 | +if __name__ == '__main__': |
| 210 | + main() |
0 commit comments