Da Ploy
It's a well known fact that we software developers are a lazy bunch. Part of what we like to do is eliminate future work for ourselves. A less well known fact, is that developers think that they're pretty smart. For the last few days i've been letting my lazy and smart streaks work together to automate my MercuryMover (The very soon to be released program that lets you move and resize windows on your Mac via the keyboard) build such that with one click i can:
- build a specific distribution
- build a specific version
- create an installer package
- zip the package
- push it to my website
- update the download link
- update the appcast
Not only will this save me a lot of (very valuable) time as i frequently spin new builds in the run up to launch, but more importantly i'm certain that this procedure is going to save my posterior many times in the future. The build is designed to be fool proof (perfect for a developer like me) and prevent me from making dumb yet costly mistakes as i scramble to get a fix out.
I thought that i'd briefly describe each step and give the how and why in order to help out some other lazy people like me.
Build a Specific Distribution
I have two distributions: Pre-Release and Release. There is a corresponding xcode build configuration for each. Pre-Release builds expire 60 days after they are built (see this post from Daniel Jalkut for some background). To make that work, i have a build setting called EXPIREAFTERDAYS which is 60 in the Pre-Release configuration and 0 everywhere else. Whenever MercuryMover is invoked, it'll execute the following:
if (EXPIREAFTERDAYS > 0) {
NSString* nowString =
[NSString stringWithUTF8String:__DATE__];
NSCalendarDate* nowDate =
[NSCalendarDate dateWithNaturalLanguageString:nowString];
NSCalendarDate* expireDate =
[nowDate addTimeInterval:(60*60*24* EXPIREAFTERDAYS)];
if ([expireDate earlierDate:[NSDate date]] == expireDate)
{
isExpired = YES;
}
}
Now when i first tried this, it failed and there are a few (3 by my count) builds of MyWi (MercuryMover's gestational name) out there that will never expire. Any enterprising developers out there know why? That EXPIREAFTERDAYS is just a build setting. As such it doesn't mean anything to the class where i implemented the check. To hook that up, you need to edit the value "Preprocessor Macros" for all configurations to say this:
EXPIREAFTERDAYS=$(EXPIREAFTERDAYS)
As you can imagine, this is like adding a #define EXPIREAFTERDAYS to my classes. Without this kind of automation, i wouldn't try to have self expiring builds. There would be too great a chance that one would get deployed to my site, and there would N ticking time bombs out on the internets where N is the number of people that have purchased a license and are running an expiring build.
Build a Specific Version
I wanted my build and deploy script to take the version as a parameter so that i could simplify keeping the version in sync everywhere and also to enforce my self imposed source control policies. Before i developed this build, i just had an xcode target that would push a build to my site. More than once i would push the build (complete with version number) without bothering to tag the sources. That's all well and good while you're in beta, but after launch, i'll need to be able to determine the precise version in order to provide support.
This was trickier than i thought, but only due to path issues. The script is simple:
#!/bin/bash #first export the tagged project from svn rm -rf /tmp/MercuryMover mkdir /tmp/MercuryMover cd /tmp/MercuryMover svn export file:///usr/local/svnroot/repos/MercuryMover/tags/$1/src/MercuryMover #build cd MercuryMover xcodebuild clean xcodebuild -target Deploy.Production -configuration Pre-Release APPLICATION_VERSION=$1 SRCROOT=`pwd` SOURCE_ROOT=`pwd` echo "done"
the trickier stuff happens in a script that is run by the Deploy.Production target. Eagle eyed readers will notice that the configuration is hard coded. This was on purpose to make it really difficult to screw up.
Installer
This might be a controversial decision amongst the members of the Mac developer community, but i really can't do a drag and drop install. MercuryMover ships as a PreferencePane. Inside that PreferencePane is a program called MercuryMoverAgent.app . This agent is what listens for the hotkeys and does the window moving and resizing magic. If you currently have MercuryMover enabled, you can't even drag a new MercuryMover.prefPane to your PreferencePanes folder because MercuryMoverAgent is running. You can double click on your new version of MercuryMover.prefPane that you downloaded to your desktop and System Preferences will dutifully upgrade your existing install; however, it will not restart MercuryMoverAgent.app . Thus, in order to see the changes in the new version, you would have to manually disable and re-enable MercuryMover (which stops and restarts MercuryMoverAgent.app).
Enter Installer.app . With a pretty vanilla .pkg file i can use a preflight script to kill MercuryMoverAgent and a postflight script to start it again. Using PackageMaker, i was able to specify what goes into my .pkg via the gui which yeilds a .pmproj file. I can then use the command line packagemaker as part of my Deploy.Production run script phase:
/Developer/Tools/packagemaker -build -proj ${PACKAGEMAKER_PROJECT_FILE_PATH} -p ${PACKAGE_DESTINATION}
The only thing i couldn't figure out how to do with the command line packagemaker was set the .pkg's version number. This is an important value as Installer.app keeps receipts of the packages that you install and uses that version number to determine how to handle your .pkg. I ended up punting and getting my pyth on to update the Info.plist file inside MercuryMover.pkg/Contents/ . In a future rev, i may try to engineer a drag and drop install. When i go Leopard only, i can use FSEvent to determine when the PreferencePane changes, but for now this will have to suffice.
Zip
I went with a zip file instead of a disk image for two reasons. One, the consensus of the MacSB yahoo group seems to be that zip files are simpler for users. Two, the consensus of me is that zip files are simpler for the industrious developer. I can zip the installer with one line:
ditto -ck --sequesterRsrc --keepParent ${PACKAGE_DESTINATION} ${ZIPPED_PATH}
Push to Website
The benefit of this is obvious. Especially since it's kind of a slow step to push the bits over the wire. I made a hierarchy on my site with release and pre-release directories under /files . This is another check against accidentally deploying the wrong file. I just use vanilla scp to push the file.
Update Download Link
The zipped .pkg file has the version name embedded in it (ie MercuryMover_0.9.4.zip) in order to groove with Sparkle . However, i don't really want to update my site every time i spin a build, so i cheat with a symlink. Once the build has been pushed, i just do a:
ssh -l username heliumfoot.com "rm -f ${SYMBOLIC_LINK_NAME}"
ssh -l username heliumfoot.com "ln -s ${DESTINATION_DIRECTORY}${ZIPPED_NAME} ${SYMBOLIC_LINK_NAME}"
easy. perfect.
Update appcast.xml
Another no brainer. When i was doing these by hand, i grew to hate the appcast file. And it's so short! I got my pyth on for this one too. My script is quick and dirty but it works. I'm aided by the fact that i don't really have automatic updates, but rather automatic update checking (powered by Sparkle). The MercuryMover UI didn't really groove with the Sparkle info panels, so i rolled my own solution which uses Sparkle to check for updates, and I show my own UI. Without the Sparkle windows, i didn't need release notes which really simplified the appcast processing. I'm sure i'll put them in later when i get a better update system going, but this will be fine for now.
Apparently, i had a lot to say about my build. My wife often calls me the smug chef whenever i'm particularly happy with some dish i made. Anyone care to call me the smug deployer?
31 days until launch.