Editorial note: This entry describes at length the process I went through to achieve the “perfect” script for determining and inserting the Subversion revision number into my builds. If you’d rather skip the details and jump to the copy/pasteable script, click
here.
Around the time I first tried Subversion, I noticed an interesting mailing list post from Axel Andersson, which gave great script for using Subversion’s “revision number” as the “build number” for an Xcode project. (Note the script in the above link is unusable, because the mailing list archive stripped out some meaningful variable expansions).
For those of you aren’t familiar with Subversion, it uses a simple system of versioning, in which every commit to the repository advances the “revision number” by one. So you start off at 1 and every change you make bumps the digit. (Coming from a CVS mindset, this makes me feel a little oogy, because I’m used to committing files one at a time, even though they’re all part of a single “intent,” but I’m getting over it).
The Subversion revision number is a great candidate for the “build number” that goes next to your application’s version in the standard about box. Instead of an arbitrary number that confirms to you and users that one version is indeed later than another, this number allows you to jump back with fair certainty to the exact sources that constituted a user’s build, even if you were too lazy to go out of your way to introduce a tag corresponding to that instant.
Axel’s technique is fantastic because it takes all the work and worry out of it. Every time you build, grab the latest revision number from Subversion and inject it into the Info.plist file for the target. It took me a while to notice a subtle problem with his script, however. The number that is generated using his example (on my machine, anyway) is not reliably advanced. It doesn’t pick up the “latest” revision, it only picks up the revision of the root directory. In my current repository, the latest revision is “10,” yet the method described by Axel fails to detect this:
% svn info | grep “^Revision:”
Revision: 1
%
I can’t figure out when or if this root directory’s revision ID will change, but it certainly doesn’t change on every commit. What we want to know is, what was the revision number caused by the last checkin?
After tinkering with svn info, I discovered to my dismay that there is no option for simply spitting out the latest revision number. It depends on receiving a specific entity to report information about. In fact, if you point it at the repository itself (not the checked out copy), then it will in fact report, among other things, the latest revision number. So once you know the repository URL, you can get the latest revision number by doing something like this:
% svn info file:///Users/daniel/Sources/SVNROOT | grep “^Revision”
Revision: 10
%
But argh!, this will require that I dynamically figure out the repository address at compile time. I don’t want to hardcode it. I will have to use svn info on the root directory to obtain the repository, and then svn info again on the repository URL to obtain the revision. Too many steps! Poking around the web a little bit, I discover a hopeful lead with an example that uses the svn log command. It involves passing the –revision parameter and special revision tag “HEAD”. I tinker with this a bit and come up with something workable:
% svn log -q –incremental –revision HEAD | tail -n 1
r10 | daniel | 2005-08-14 12:25:56 -0400 (Sun, 14 Aug 2005)
%
I can get that “r10” at the beginning and parse it! Too bad it’s not the same format as Axel’s original post, though. Wait a minute – what happens if I pass the –revision parameter to svn info?
% svn info –revision HEAD | grep “^Revision: ”
Revision: 10
%
Success! Well, I thought so. Several reader comments below have convinced me to switch to svnversion. I was especially swayed by Axel’s observation that my technique won’t work as expected for repositories checked out to a specific revision number. To test this theory, I check out my project ot revision 5, and try my technique:
% svn info –revision HEAD | grep “^Revision: ”
Revision: 10
% svnversion ./
5
%
OK! I’m ready to play the svnversion game. But svnversion gets tricky when there’s any activity at all in the checked out repository. Going back to my working repository, I get output like this:
% svnversion ./
1:10M
%
Because some files in my directory have not been modified since revision 1, some were modified as late as revision 10, and some are modified but not yet checked in, I get this complex shorthand. I’m really happy with just using the right-most number, as Axel suggested in the comments area, but I decided that I might like to have the “M” too. It’s not usual to ship products with letters in the build version field, but then again, it’s not (shouldn’t be) usual to ship products with locally modified sources. I figure this might be a nice reminder and reality check. If somebody does get their hands on a version that has an “M” in it, I’ll know that it was a pre-release or one-off and I shouldn’t be too concerned about bugs that might only occur in that version.
I decide to use a Perl regular expression to parse out the desired information. I’m a regular expression “long time newbie” so I always have to pull up some kind of reference while I work with them. I’m sure I don’t do things the best way, but I get the job done and call it a day. This is what I’ve come up with as a pattern for stripping the right-most digit, as well as the “M” (or “S”) tag that might also be appended to the digits:
% svnversion . | perl -p -e “s/([\d]*:)(\d+[M|S]*).*/\$2/”
10M
%
Looks good. Now I can just plug that minor adjustment into Axel’s original script. For anybody interested in applying this to their own Subversion/Xcode project, the modified script is as follows:
# Xcode auto-versioning script for Subversion
# by Axel Andersson, modified by Daniel Jalkut to add
# "--revision HEAD" to the svn info line, which allows
# the latest revision to always be used.
use strict;
die "$0: Must be run from Xcode" unless $ENV{"BUILT_PRODUCTS_DIR"};
# Get the current subversion revision number and use it to set the CFBundleVersion value
my $REV = `/usr/local/bin/svnversion -n ./`;
my $INFO = "$ENV{BUILT_PRODUCTS_DIR}/$ENV{WRAPPER_NAME}/Contents/Info.plist";
my $version = $REV;
# (Match the last group of digits and optional letter M/S):
# ugly yet functional (barely) regex by Daniel Jalkut:
#$version =~ s/([\d]*:)(\d+[M|S]*).*/$2/;
# better yet still functional regex via Kevin "Regex Nerd" Ballard
($version =~ m/\d+[MS]*$/) && ($version = $&);
die "$0: No Subversion revision found" unless $version;
open(FH, "$INFO") or die "$0: $INFO: $!";
my $info = join("", <FH>);
close(FH);
$info =~ s/([\t ]+<key>CFBundleVersion<\/key>\n[\t ]+<string>).*?(<\/string>)/$1$version$2/;
open(FH, ">$INFO") or die "$0: $INFO: $!";
print FH $info;
close(FH);
To add this script to your build process, you simply create a new “Shell Script Phase” at the end of your target’s build phases, and copy & paste the contents from above into that phase. The script will edit your freshly-copied Info.plist file in place to reflect your latest Subversion revision.
Make sure you specify Perl as the shell for the script, I use “/usr/bin/perl -w” on my machine.
If any Subversion nerds stumble upon this entry and can suggest a better way of obtaining the latest revision for the project, please let me know!