Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@bruienne
Last active April 16, 2021 05:29
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save bruienne/f9fc1c7039f137a49203 to your computer and use it in GitHub Desktop.
Save bruienne/f9fc1c7039f137a49203 to your computer and use it in GitHub Desktop.
Get OS X version from DMG
#!/usr/bin/python
#
# getosversionfromdmg.py
#
# Copyright (c) 2014 The Regents of the University of Michigan
#
# Retrieves the OS version and build from the InstallESD.dmg contained in
# a typical "Install (Mac) OS X <Name>.app" bundle.
#
# To run:
# ./getosversionfromdmg.py \
# "/Applications/Install OS X <Name>.app/Contents/SharedSupport/InstallESD.dmg"
#
# Two modes:
# - When run without sudo it reports the version and build to STDOUT
# - When run with sudo it reports the version and build to STDOUT and renames
# the DMG to "InstallESD_<version>_<build>.dmg"
#
# Based on code written for the Munki project by Greg Neagle:
# https://code.google.com/p/munki/
import os
import sys
import tempfile
import subprocess
import plistlib
def cleanUp():
'''Cleanup our TMPDIR'''
if TMPDIR:
shutil.rmtree(TMPDIR, ignore_errors=True)
def fail(errmsg=''):
'''Print any error message to stderr,
clean up install data, and exit'''
if errmsg:
print >> sys.stderr, errmsg
cleanUp()
# exit
exit(1)
def mountdmg(dmgpath, use_shadow=False):
"""
Attempts to mount the dmg at dmgpath
and returns a list of mountpoints
If use_shadow is true, mount image with shadow file
"""
mountpoints = []
dmgname = os.path.basename(dmgpath)
cmd = ['/usr/bin/hdiutil', 'attach', dmgpath,
'-mountRandom', TMPDIR, '-nobrowse', '-plist',
'-owners', 'on']
if use_shadow:
shadowname = dmgname + '.shadow'
shadowroot = os.path.dirname(dmgpath)
shadowpath = os.path.join(shadowroot, shadowname)
cmd.extend(['-shadow', shadowpath])
else:
shadowpath = None
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)
if pliststr:
plist = plistlib.readPlistFromString(pliststr)
for entity in plist['system-entities']:
if 'mount-point' in entity:
mountpoints.append(entity['mount-point'])
return mountpoints, shadowpath
def unmountdmg(mountpoint):
"""
Unmounts the dmg at mountpoint
"""
proc = subprocess.Popen(['/usr/bin/hdiutil', 'detach', mountpoint],
bufsize=-1, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(unused_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 getosversioninfo(mountpoint):
""""getosversioninfo will attempt to retrieve the OS X version and build
from the given mount point by reading /S/L/CS/SystemVersion.plist
Most of the code comes from COSXIP without changes."""
# Check for availability of BaseSystem.dmg
basesystem_dmg = os.path.join(mountpoint, 'BaseSystem.dmg')
if not os.path.isfile(basesystem_dmg):
unmountdmg(mountpoint)
fail('Missing BaseSystem.dmg in %s'% mountpoint)
# Mount BaseSystem.dmg
basesystemmountpoints, unused_shadowpath = mountdmg(basesystem_dmg)
basesystemmountpoint = basesystemmountpoints[0]
# Read SystemVersion.plist from the mounted BaseSystem.dmg
system_version_plist = os.path.join(
basesystemmountpoint,
'System/Library/CoreServices/SystemVersion.plist')
# Now parse the .plist file
try:
version_info = plistlib.readPlist(system_version_plist)
# Got errors?
except (ExpatError, IOError), err:
unmountdmg(basesystemmountpoint)
unmountdmg(mountpoint)
fail('Could not read %s: %s' % (system_version_plist, err))
# Done, unmount BaseSystem.dmg
else:
unmountdmg(basesystemmountpoint)
# Return the version and build as found in the parsed plist
return version_info.get('ProductUserVisibleVersion'), \
version_info.get('ProductBuildVersion')
TMPDIR = None
def main():
"""Docstring"""
global TMPDIR
# Spin up a tmp dir for mounting
TMPDIR = tempfile.mkdtemp(dir=TMPDIR)
if len(sys.argv) < 2:
print '\nNo path to an InstallESD.dmg provided, stopping.\nInvoke with "getosversionfromdmg.py <path to InstallESD.dmg>" and try again.\n'
sys.exit(-1)
dmg = sys.argv[1]
dmgfilepath, ext = os.path.splitext(dmg)
print 'Getting OS version and build for ' + dmg + "\n"
mountpoints, shadow = mountdmg(dmg)
for mount in mountpoints:
if mount.find('dmg'):
os_version, os_build = getosversioninfo(mount)
print 'OS X Version: ' + os_version + '\n' + 'OS X Build: ' + os_build + '\n'
unmountdmg(mount)
if dmgfilepath.endswith('InstallESD') and os.getuid() == 0:
print "We're running as root so we'll rename the DMG to reflect its OS version and build.\n"
os.rename(dmg, dmgfilepath + '_' + os_version + '_' + os_build + '.dmg')
elif not dmgfilepath.endswith('InstallESD') and os.getuid() == 0:
print "Looks like the DMG file was previously renamed so we're skipping that step."
else:
print "Not running as root so we're not renaming " + dmg + " to contain the version and build."
print "Re-run this tool with sudo to rename it.\n"
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment