Apple Packages
Introduction & Overview
The concept of software packages is a simple one. Bundle the necessary files for a program into a single file that can be read and understood by an installer tool which then copies the files to their proper locations and does any necessary steps to enable, automate, or otherwise perform some action concerning the new software using scripts of one form or another.
I am biased in favor of native format packages over third party distribution solutions, being a huge fan of both Apple’s .pkg and .mpkg systems and Debian’s .deb format. Each is structured differently but performs the function described above.
Building packages is a great way for an administrator to save time, especially for large software deployments, and it gives a trackable (package installs are logged and generally create some form of ‘receipt’, which details every file installed and documents what scripts are run) installation, which makes auditing the software and filesystem on a machine, well, not necessarily easy but easier than trying to keep track of everything and do all the accounting by hand.
Benefits
Packages are the only form of complex distribution method supported by Apple’s Remote Desktop software. While this was enough of a reason for me, there are those who would require more of a reason to invest any amount of time into learning about packages and how they can help streamline any administration task. To many, the “Copy To…” and “Send to UNIX…” commands represent all they need to do their job, and I am not about to tell them that they are wrong.
Architecture
The Number One Thing to keep in mind when reading the next section is that any .pkg or .mpkg you create is just like any Application, Keynote document, Pages document, OmniGraffle Document, or anything else saved as a Bundle. Its nothing magical; it contains no executable binary, unless you really want to get technical and count any images or the main compressed archive in which everything is stored. It is a Bundle, which is a concept alien to Windows users, new Mac Users, and even some advanced Mac Geeks.
Bundles
Bundles are no more than a Folder with a special name and structure. Take any folder you have, and rename it by adding a “.pkg”, “.wdgt” or .”bundle”, go ahead and try it, I’ll wait. Cool, eh? You can even try opening it at this point, but since that renamed folder is missing several key pieces, the program probably won’t open it, or will throw an error. There is nothing special about that folder other than its name, rename it again and remove the .whatever and it’ll change back to a normal folder again. Finder knows that a folder that ends in .pkg is a Package, just like it knows that any File ending in .jpg is a JPEG Image. Since it knows the what, it also knows the who, at least in terms of what Application gets the honor of opening it. That, in very simple terms, is what constitutes a bundle, its just a folder, with a collection of files in specifically defined locations, named with a special extension.
.pkg and .mpkg bundles are opened by Installer.app, just like .jpg and .gif are both opened by Preview.app (by default). If you have ever went poking around the internals of OS X, you might have stumbled across Installer in /System/Library/Core Services/, which also houses many other useful apps that we all take for granted like Archive Utility.app, Dock.app, loginwindow.app, and Finder.app.
Installer.app merely reads the configuration files present, and if necessary presents the user with the installer interface. Its actually a little more complex than this, but nothing really relevant to our cause. Its a sidebar I may tack on later if I find a way to shoe horn it in For now, suffice it to say that Installer.app handles the GUI of package installs when one is needed, though there are other forces that can also read and interact with packages as needed (ex: command line tools).
Order of Operations
Packages are very sequential, and there is good reason for this. They must consistently execute the same way each time, on each system, with as little variation as possible. Any variation that *can* happen needs to be predicted and handled (user changing the install location, a prerequisite file or program not being found , etc.
Apple’s packages execute in this order:
- Installer.app launches and opens the pkg or mpkg file
- Check Scripts (InstallationCheck and VolumeCheck) both fire sequentually
- User steps through the GUI as needed
- User Clicks Install
- Pre-Execute Scripts (preflight)
- Pre-Install / Pre-Upgrade Scripts
- Decompress / Deploy Files (using the Archive.gz and/or .bom files)
- Post-Install / Post-Upgrade Scripts
- Post-Execute Scripts (postflight)
- User Clicks Complete
On OS X, to give developers the most flexibility, Apple left the scripting language up to the deployer. I prefer Perl scripts, but Bash, Python, Applescript, Ruby, PHP and many many others will all work, and each have their strengths and pitfalls concerning this task.
Also of note is that “Requirements” can be set within the package that are outside the purview of the scripts. Often enough these built in requirements checks are enough, but where they are not, there is the combination of scripting and strings files (files containing keyed error messages) to step in and do any heavy lifting. The requirements are stored inside the Package’s Info.plist file. If any requirements are set within Iceberg or PackageMaker, they will override InstallationCheck and VolumeCheck, even if both scripts are present.
Check Scripts
InstallationCheck
If you have ever run a package and then immediately gotten a prompt “This Package contains an application which will determine if the software can be installed on this computer” it was tripped by this script being present. It is basically something that runs before anything else is done. It an VolumeCheck are the only two scripts in the whole process that let you provide decent feedback messages (see InstallationCheck.strings below) to the user regarding issues with your install and their system, so these are the two scripts you should be using to do any kind of checking in that regard.If this script exits with a value other than zero, it is considered a failure and the install is aborted. If InstallationCheck exits with a value other than zero, different things can happen. If the script exits with a value of 33, Installer displays a generic warning, but the install can continue. If the script exits with a value between 34 and 47, Installer will display a warning with a more specific message, but its still a generic, canned Apple warning. Exiting with a value between 48 and 63 allows you to define custom warning messages in your .strings file. Up to value 63, the messages are simple warnings, and the user is allowed to continue the install. From value 64 on, the exit codes represent full-on errors, which result in an error message being displayed and the install being aborted. 64, like 32, is a generic error. 65-111 are specific error messages reserved by Apple. 112-127 are definable in the .strings file for use by the packager. More information, including a table for all scripts, their arguments, and return codes is available here.
#!/usr/bin/perl
my $sysVersion = `sw_vers | grep 'ProductVersion' | awk '{ print $2 }'`; # Sets $sysVersion to the current system's OS (ex. 10.5.7)
print $sysVersion;
if ($sysVersion =~ m/10.5./)
{
# Do Stuff
print "Doing Stuff\n";
} else {
print “Failed\n";
exit 1;
}
That would basically check if you’re running Leopard, if not, the updater fails. A more complex regular expression could check for point releases, etc.
There are many other uses for this script:
- Check if only a single user is logged in
- Check for the presence of necessary files or applications on the drive
- Check the version of the OS, versions of installed software, etc
- Etc
For a particular Office 2004 Update I was re-packaging for deployment, I needed to check that the previous update had been applied (thank you cascading requirements). Since I installed the updater log files with each package I published, I was able to do this:
#!/bin/bash if [ -e "/Applications/Microsoft Office 2004/Updater Logs/11.3.7 Update Log.txt" ] then exit 0; else exit 112; fi
…And it worked beautifully. This is also a good example of using a Strings file.
InstallationCheck.Strings
.Strings files are just short text files containing error codes and corresponding custom error messages to display. They are used for localization, as well as presenting a more informative error message to users. As in the above example, if the script fails, I exit with a status of 112. This is where Apple’s deployment guidelines get tricky, as this particular area is kind of cryptic. Different exit codes correspond to different lines in the string file but the relationship is not straightforward. See this Source for a more the official explanation.
The long version is that they want you to use a 7-bit (128 possible values) code for error messages, but you can only use 4 of those 7 bits (bits 5 and 6 are always 1 and bit 7 appears to always have to be 0) , giving you 30 possible custom warning and error messages. They further say that you’re only to use 15 of them (messages 16-31), reserving the rest for their use (1-4 are pre-canned Apple responses). So anyway, your 7-digit code for message 16 looks like:
0111000
Which is 112 in decimal.
Here’s the short version:
Warning Exit Code = Error Exit Code = Strings Message ID 48 = 112 = 16 49 = 113 = 17 50 = 114 = 18 51 = 115 = 19 ... 63 = 127 = 31
If you exit with either 48 or 112, message 16 gets displayed. So again, you get 15 possible warning and error messages to play with, even though you get 30 possible exit codes. Confused yet?
Example InstallationCheck.strings file:
"16" = "This Update Cannot be Installed Until Microsoft Office 2004 11.3.7 is installed"; "17" = "Please Close all Office Applications Before Continuing”;
Both of these files just need to be present in the package’s Resources directory in order to work.
VolumeCheck
VolumeCheck determines if a candidate volume is a suitable destination for the package. This lets you be selective and disqualify alternative volumes a user might select. You can, for example, require certain files/folders to exist in particular locations. The script receives one argument, which is the UNIX path to the volume (“/”, “/Volumes/VolumeName”, etc). You can pretty much do whatever you want inside of the script, and simply exit with a status of 0 if the volume meets your specifications, any other number if it does not. Just like InstallationCheck, there are a series of exit statuses (from 112 to 127) that you can return to spell out exactly WHY a Volume doesn’t meet spec, which can prove handy from time to time. Like InstallationCheck, VolumeCheck (along with the optional corresponding .strings file) just has to be present inside the package’s resources directory for it to work.
Sample
A sample Iceberg package containing an InstallationCheck and a VolumeCheck script, and their corresponding .strings files is available here.
A similar sample package built using PackageMaker, is available here.
Pre/Post Scripts
The other 6 possible scripts a package can run all center around performing tasks related directly to the install. These can handle stopping a service or unloading a daemon, etc. If a script exits with a non-zero status, the Install is considered Failed, and will report as such to the user. However, unlike VolumeCheck and InstallationCheck, there’s no good way to provide feedback from these scripts (technically you probably *could* provide some type of feedback via an osascript call (`osascript -e 'tell application "Installer" to display dialog "The Install Failed because of <blah>"'`) but I highly doubt Apple would consider this kosher, and I definitely would not, as it would break when deploying the package via ARD.
Apple Remote Desktop (and other remote deployment methods)
Developers should care about how their packages behave not just when run from a GUI, but when run from the command line, and when deployed through Apple Remote Desktop. Scripts can react differently when there’s no console session, packages can cause chaos if they prompt for user input when they’re not supposed to. Good error messages built into the .strings files get returned to ARD if the package errors when its being deployed. They make troubleshooting packages at a glance much easier. Non-cryptic error codes save everybody from having to chase down error messages in system logs and other slums.
Tools
Apple provides its own PackageMaker tool, which evolved considerably in the past few years, but dollars to donuts, Iceberg still trumps it from a usability perspective, hands down. From the ability to place files precisely where you want them to go, and set permissions and flags on them, to the painless interface to add scripts, resources, and requirements, Iceberg offers a much more rapid and sane solution. The one area where I will concede that PackageMaker wins, is in the automation category. With a decent .pmdoc file (the saved file format of PackageMaker), you can integrate with Xcode via scripts and have your deployment/production build process not only build your application, but also insert it into a package and build the package for you as well. Honestly, Iceberg may have similar capabilities, I haven’t played much with its command line featureset.
Leopard
Leopard brought several changes to the Installer application, namely a new format that eschews the coveted Bundle format for an archive format called Xar (similar to Zip, Tar, Rar, etc). It allows for more streamlined distribution (no more having to nest inside of a disk image (dmg)) If you bust open one of these new .pkg or .mpkg files (using command line tools or Apple’s GUI Tool), you’ll find some metadata files (plists) and if its a metapackage you’ll probably find a few good old 10.4-era packages inside. Extract these out and you’ll find them to be more or less the same .pkg bundles I have been explaining. This may not always be the case, the new format has a similar structure, but some things have been moved around, and as developers and packagers stretch into the new areas the package format will evolve like everything else.
Resources
- Installation/VolumeCheck Demo
- An Iceberg project that demonstrates InstallationCheck and VolumeCheck, as well as .strings file for each. The package has a preflight script that returns non-zero, so the package will always fail. This is intentional.
- PackageMaker Demo
- An PackageMaker project that compliments the above Iceberg project. The package has a preflight script that returns non-zero, so the package will always fail. This is intentional.
- The Apple Software Delivery Guide
- Stéphane Sudre’s Deployment Guidelines
- Stéphane Sudre’s PackageMaker How-To
- Iceberg
- Apple’s Installer Dev Mailing List
Re. the command-line version, freeze: it worked fine for me. In Terminal it didn't need a path, but in Automator it did, so when I replaced 'freeze MyInstaller.packproj' with '/usr/local/bin/freeze MyInstaller.packproj', it worked. There are two optional flags: -v, verbose; -d, scratch folder – supply the folder as the argument, presumably.
Thanks Robin, I've heard similar things from guys on the Installer-Dev list, I've just never had a reason to use it so I didn't want to praise or bash it, given my ignorance. I have never used PackageMaker's CLI tool either, but I've poked around it enough and read enough threads about frustration that I know the basics of it. I plan on revising those comments and others in the near future, I'll probably try to work up some test cases for both PM and Iceberg to tinker with the command line building stuff.