Tuesday, March 3, 2015

Finding the OS X version and build information in an Install*OS X*.app

The problem

If you have downloaded [Mac] OS X from the the Apple App Store, the installer is stored to the folder called /Applications and the application name is equal for all update releases of a particular OS X version. For example, the Yosemite installer can be found in "/Applications/Install OS X Yosemite.app" and if you open it, it provides information that it is going to install OS X 10.10. Unfortunately it doesn't tell you whether it is going to install OS X 10.10, OS X 10.10.1 or OS X 10.10.2. In other words, the GUI is not suitable to determine the exact OS X version that the installer is loaded with. Both the update number and the build number are not visible. Screenshot below shows the installer of 10.10.2:



You can find the OS X version and build information of your running OS X, see also https://support.apple.com/en-us/HT201260 - IMHO it should be possible to get those details before an actual installation as well.

So the question is how to find the complete OS X product version and build version in an "Install*OS X*.app package?

Eleven code lines to success

The command line tool called sw_vers can print out both the product version and the build version of an installed product. Example from Mavericks:

$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.9.5
BuildVersion:   13F34

The idea is to get access to the sw_vers respectively the version information that is stored in the installer image.

At first we need to mount the InstallESD.img that is stored in /Applications/Install*OS X* Installer.app/Contents/SharedSupport. Note that in the example below, the environment variable APPNAME has to be set to the name of the installer app, in case of Yosemite it is "Install OS X Yosemite.app". Also set DEBUG to /dev/stdout in order to see the output of the mount actions. The $$ will be replaced by the pid of the shell which will be our unique number for the session in order to avoid collisions with other potential volumes.

hdiutil attach "/Applications/$APPNAME/Contents/SharedSupport/InstallESD.dmg" -noverify -nobrowse -mountpoint /Volumes/InstallESD.$$ > $DEBUG

Once that is done, we can mount the BaseSystem.dmg

hdiutil attach "/Volumes/InstallESD.$$/BaseSystem.dmg" -noverify -nobrowse -mountpoint /Volumes/BaseSystem.$$ > $DEBUG

Now we have access to sw_vers. However, calling the /Volume/BaseSystem.<pid>/usr/bin/sw_vers won't give us the results we expect, though. Actually it still gathers version information from the host system and not from the mounted volume. That is because the version number is read from the absolute path called /System/Library/CoreServices/SystemVersion.plist. Below is an excerpt from the SystemVersion.plist on the host system running Mavericks.

<key>ProductBuildVersion</key>
<string>13F34</string>
<key>ProductCopyright</key>
<string>1983-2014 Apple Inc.</string>
<key>ProductName</key>
<string>Mac OS X</string>
<key>ProductUserVisibleVersion</key>
<string>10.9.5</string>
<key>ProductVersion</key>
<string>10.9.5</string>

If we use a changed root environment in order to let read sw_vers the correct file sounds like an easy solution, but tests have shown that it didn't work with the latest Yosemite installer. Well, why not simply mimic the sw_vers functionality by reading the xml file called /Volumes/BaseSystem.$$/System/Library/CoreServices/SystemVersion.plist using bash's regular expression build-in features? That approach is even faster than initiating a changed root environment, root permissions are not required and it is more comfortable than to print out the entire xml:

XMLCONTENT=$(<"/Volumes/BaseSystem.$$/System/Library/CoreServices/SystemVersion.plist")
if [[ "$XMLCONTENT" =~ \<key\>ProductVersion\</key\>[[:space:]]*\<string\>([0-9\.]+)\</string\> ]]; then
    printf "ProductVersion: %s\n" ${BASH_REMATCH[1]}
fi

XMLCONTENT stores the content of the plist file and the first expression in the brackets resp. ${BASH_REMATCH[1] stores the complete product version of OS X.

In order to extract the build version information, we can enter:

if [[ "$XMLCONTENT" =~ \<key\>ProductBuildVersion\</key\>[[:space:]]*\<string\>([0-9A-Z]+)\</string\> ]]; then
    printf "BuildVersion:   %s\n" ${BASH_REMATCH[1]}
fi

If the desired information has been gathered, we just need to unmount the two volumes again in reverse order for cleanup purposes:

hdiutil detach "/Volumes/BaseSystem.$$" > $DEBUG
hdiutil detach "/Volumes/InstallESD.$$" > $DEBUG

The solution

Put the eleven code lines above to a small script called osxapp_vers, set execute permissions to it, set APPNAME to an appropriate value and set DEBUG to /dev/null ...

chmod +x ./osxapp_vers
export APPNAME="Install OS X Yosemite.app"
export DEBUG="/dev/null"

... and you can determine the Installer OS X app's product version and build version just by calling:

./osxapp_vers
ProductVersion: 10.10.2
BuildVersion:   14C109

Note: I have tested the solution above on all my [Mac] OS X downloads from the Apple App Store and I can confirm that this article applies at least to Mac OS X 10.7.5 (Lion) until OS X 10.10.2 (Yosemite).

Mission completed ;-)

Update on  March 4, 2015

The build version is not a hex value and therefore the regular expression has to be ([0-9A-Z]) instead of ([0-9A-F]). I have fixed the bug in the code above.