|
Volume Number: 19 (2003)
Issue Number: 8
Column Tag: Programming
Self-Installing Applications
Or "How I became Installer Free"
by Kenneth H. Wieschhoff, Jr.
Introduction
The last step in any software development process is to create an installer. This can be Apple's PackageMaker, InstallerVise by MindVision, and the like. By making your application self-installing you can update your frameworks, libraries, (and even kernel extensions) dynamically prior to running your main application. Your users can run your application anywhere and you can include incremental updates to your software. This makes a more enjoyable user experience.
The Secret
In short.... Bundles! To the user your application looks like a single item, but in reality it can be an entire suite of tools. You create an Installer application that checks your support files (frameworks, kexts, libraries, et. al.) are present and up to date. Missing/outdated items are installed using Apple's Authorization Services API. Finally, this Installer launches the main application and quits. You store all the items, including the main application, in the Resources Folder within the Installer application.
The Steps
Here's a checklist of thing to do:
Create the self-installer application. Add your main application's icon.
Write code in main to verify your support items are installed and up to date, and then launch the main application.
Create the scripts which will install a given support item. Authorization Services will run these scripts for you at a privileged level.
Create "move" scripts to assist the build process, which move your support files and main application from their respective build directories into the Resources folder within the Installer.
Create the Self-Installer application
When I'm developing a software product that contains many different parts, I usually create a folder to hold all the pieces. In addition to making source control easier to manage, it will simplify creating the "move" scripts. More on that later.
Here's an example of a folder layout for MyApp:
MyApplication MyApp (the main (real) application) MyFramework MySelfInstaller (also produces an application called MyApp)
You'll use Project Builder to create a new "Cocoa Application" in your (new) MySelfInstaller directory. Let's have a look at the main code:
Listing 1: main.m
main.m Check if support items exist and are up-to-date and then launch the main application. #import <Cocoa/Cocoa.h> #include <Security/Authorization.h> #include <Security/AuthorizationTags.h> #include <stdio.h> #include <unistd.h> #include <sys/param.h> bool CheckItemUpToDate(char *where, char *what); OSStatus DoInstall(char *script); void LaunchMainApplication(); AuthorizationRef gAuthorizationRef = 0; int main(int argc, const char *argv[]) { NSAutoreleasePool *pool=[[NSAutoreleasePool alloc] init]; OSStatus stat = noErr; if ( !CheckItemUpToDate("/Library/Frameworks/", "MyFramework.framework")) stat = DoInstall("InstallFramework.sh"); if ( stat == noErr) LaunchMainApplication(); // Release the authorization reference if(gAuthorizationRef) AuthorizationFree(gAuthorizationRef, kAuthorizationFlagDefaults); [pool release]; return (EXIT_SUCCESS); }
Main checks that the framework is installed on the user's system and installs it if it's out of date or missing. It then launches the main application and then quits. (Noticeably absent in this example is some way to alert the user that installation of an item failed.)
Listing 2: main.m
main.m The CheckItemUpToDate checks to see if a given support item is up to date. bool CheckItemUpToDate(char *where, char *what){ // Guilty until proven innocent. bool installNotNeeded = false; // Create the path to the installed support item. NSString *fullPath = [NSString stringWithFormat:@"%s%s", where, what]; // Get the url of the "SupportItems" folder // within the Resources folder of our application CFURLRef url = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("SupportItems"), NULL, NULL); // Get the path to the support item within // our own Resources bundle NSString *supportPath = [NSString stringWithFormat:@"%@%s", [((NSURL *)url) path], what]; // Get the bundle of the installed support item NSBundle *instExt = [NSBundle bundleWithPath:fullpath]; // If the extension exists... if (instExt) { //...load it's dictionary. NSDictionary *installedDict = [instExt infoDictionary]; // If the dictionary exists... if (installedDict) { // Get the short version string NSString *version = [installedDict objectForKey:@"CFBundleShortVersionString"]; if (version){ // Do the entire thing again for the support tool NSBundle *local = [NSBundle bundleWithPath:toolpath]; if ( local) { NSDictionary *locDict = [local infoDictionary]; if (locDict) { NSString * locVers = [locDict objectForKey: @"CFBundleShortVersionString"]; // Compare the two strings for a match. if ([locVers compare:version] ==NSOrderedSame) installNotNeeded = true; } } } } } return installNotNeeded; }
CheckItemUpToDate compares the Short Version Strings of the installed item against the item packaged in the bundle. Updating the version string causes the item to be re-installed. Up to date items are not re-installed.
The MAGIC
The Authorization Services API provides a way to execute a script that needs privileges.
Listing 3: main.m
main.m DoInstall gets authorization from the user to execute a script at a privileged level and executes the script. OSStatus DoInstall(char *scriptName) { char myToolPath[MAXPATHLEN]; char *myArguments[2] = {NULL, NULL}; FILE *myPipe = NULL; char myReadBuffer[128]; AuthorizationFlags myFlags = kAuthorizationFlagDefaults; AuthorizationItem myItems[] = { // For this example we're using the standard // command interpreter {kAuthorizationRightExecute, strlen("/bin/sh"), "/bin/sh", 0}}; AuthorizationRights myRights = { sizeof(myItems)/sizeof(AuthorizationItem), myItems }; OSStatus myStatus; // Tell the developer what script is being run printf("Running install script %s\n", scriptName); // Store the authorization in a global so we don't keep // asking the user for it once it's given. if ( gAuthorizationRef == 0) { // Create the authorization reference. myStatus = AuthorizationCreate ( NULL, kAuthorizationEmptyEnvironment, myFlags, &gAuthorizationRef); if(myStatus != errAuthorizationSuccess) goto bail; // Set flags to create our authorization environment. // Request to be pre-authorized to run any tool. myFlags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagPreAuthorize | kAuthorizationFlagExtendRights; // This puts the Authorization dialog on the screen // and adds the authorization rights to the // authorization reference, gAuthorizationRef. myStatus = AuthorizationCopyRights ( gAuthorizationRef, &myRights, NULL, myFlags, NULL ); if(myStatus != errAuthorizationSuccess) goto bail; } // If we have a valid authorization reference... if ( gAuthorizationRef) { myFlags = kAuthorizationFlagDefaults; // Get the path for the script to run myStatus = GetScriptPath(myToolPath, scriptName); if(myStatus) goto bail; myArguments[0] = myToolPath; // Finally, execute our script. myStatus = AuthorizationExecuteWithPrivileges ( gAuthorizationRef, "/bin/sh", myFlags, myArguments, &myPipe); if(myStatus == errAuthorizationSuccess) { for(;;) { // Scripts send output back through myPipe which is // redisplayed on standard out int bytesRead = read (fileno(myPipe), myReadBuffer, sizeof(myReadBuffer)) // No more data! if(bytesRead < 1) break; write(fileno(stdout), myReadBuffer, bytesRead); } } else printf("AuthorizationExecuteWithPrivileges " "returned %ld\n", myStatus); } bail: return myStatus; }
DoInstall creates an Authorization reference if it doesn't already exist, and uses that authorization reference to allow the user to add privileges to execute the shell script passed to it. In addition, any output produced by the script will be echoed on standard out. This can be a valuable debugging aid when a script is giving you trouble as you can use standard shell commands like "echo" and "pwd" in your script and see the output in your Run window in Project Builder.
Launch the application
This is accomplished by calling [NSWorkspace launchApplication].
Listing 4: main.m
main.m LaunchMainApplication gets a path to the main application located within the bundle and launches it. void LaunchMainApplication() { // Get the ref to the main app in the resources folder CFURLRef url = CFBundleCopyResourceURL( CFBundleGetMainBundle(), CFSTR("MyApp.app"), NULL, NULL); NSString *path = [NSString stringWithFormat: @"%@Contents/MacOS/MyApp", [((NSURL *)url) path]]; [[NSWorkspace sharedWorkspace] launchApplication:path]; }
An installer script
Once the user has granted the application permission to perform privileged commands, you can do anything you need to install your support item in the script.
***Warning***. Your script now has omniscient powers! You can cause irreparable damage to the user's system in your script if you're not careful.
Listing 5: InstallFramework.sh
InstallFramework.sh Here's an example script which copies a framework to the system and calls update_prebinding to calculate the locations of functions within a library so applications will launch faster. #!/bin/sh # check if the global frameworks directory exists, # create it if not if [ ! -d /Library/Frameworks ]; then # create mkdir /Library/Frameworks # set group to the administrative group chgrp staff /Library/Frameworks # allow group users to modify chmod 775 /Library/Frameworks fi # make sure the framework exists within our application if [ -d \ MyApp.app/Contents/Resources/SupportItems/\ MyFramework.framework ]; then # check if the framework exists in /Library/Frameworks # delete it if it is if [ -d /Library/Frameworks/MyFramework.framework ]; then rm -rf /Library/Frameworks/MyFramework.framework fi # copy our framework to the system cp -Rp \ MyApp.app/Contents/Resources/SupportItems/\ Myframework.framework \ /Library/Frameworks # change file modes on the new framework chmod -R ogu+r \ /Library/Frameworks/MyFramework.framework # call update_prebing /usr/bin/update_prebinding -files \ /Library/Frameworks/MyFramework.framework else # Perhaps you forgot to add the file to the framework? echo MyFramework.framework is missing from build! fi
Last Step - "Move" Scripts
When you're building the Installer, Project Builder makes it easy for you to move your support items and main application into the Resources folder by adding an extra step to the build process. Select the Targets tab on the main project window and select the <MySelfInstaller> target. From the Project Menu, select "New Build Phase" and the "New Shell Script Build Phase" submenu item.
In the "Shell:" text area enter "/bin/sh" if this is the shell you normally work with. (Note you can use any shell you'd like). In the second text area enter "exec ./moveMyFramework.sh". Let's look at an example of the script:
Listing 6: moveMyFramework.sh
MoveMyFramework.sh This script copies the framework from a sibling directory into our SupportItems folder. #!/bin/sh # if the framework already exists, delete it if [ -d "build/MyApp.app/Contents/Resources/\ SupportItems/MyFramework.framework" ]; then rm -rf \ "build/MyApp.app/Contents/Resources/\ SupportItems/MyFramework.framework" fi # Check the "SupportItems" folder actually exists if [ ! -d \ "build/MyApp.app/Contents/Resources/SupportItems" ]; then mkdir \ "build/MyApp.app/Contents/Resources/SupportItems" fi # Finally, copy the framework cp -Rp ../MyFramework/build/MyFramework.framework \ build/MyApp.app/Contents/Resources/SupportItems
Don't get confused by the multiple references to "MyApp". Remember, the goal is to make the user unaware they're actually running multiple applications. You'll want to add your application's icons to the self-installer to complete the effect.
Conclusion
This approach has numerous advantages to rolling out new versions of your application and provides a very nice user experience, as the user only has to authorize once during the initial run of the application. Absent is a way to un-install the application and supporting files. This is left as an exercise for the reader.
References:
Authorization Services Reference:
http://developer.apple.com/documentation/Security/Reference/authorization_ref/
Ken Wieschhoff lives near Atlanta and works for Altea Therapeutics by day as an 8051 embedded programmer (and some Windows MFC/GUI stuff) and Eskape Labs by night where he does Cocoa programming (and occasionally device drivers) for their MyTV product line. Weekends you can find him scuba diving, riding his new Honda Valkyrie Rune, pickin' a guitar and eating grits. He can be reached at weesh@mindspring.com.
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine