Git and XCode: A git build number script
Git has been gaining in popularity with Cocoa developers as well as open source developers. As I work it into my development workflow, one item that was missing was the automatic injection of the build number into the application bundle.
There are a few scripts floating around that perform this trick for subversion, but git handles build numbers a bit differently and it appears that no one has bothered to publish one. As is known, subversion uses an incrementing integer for build numbers. This makes it very easy to determine which build number came first and makes it very useful for a non-public version number. Git, however, uses a hash for each build number which is not incrementing and therefore not very useful for determining version numbers. However, it is still very useful for pulling up a specific build when a crash report is received, etc.
Therefore, with the help of Matt Long’s perl-fu, I have updated Daniel Jalkut’s subversion perl script to work with git. Since the build numbers are not sequential, I would not recommend using them for Sparkle. Therefore, in my own build process for iWeb Buddy, I hand select the version number (for example 1.0.4) and then use the short hash from git as the CFBundleVersion number. Normally this number is displayed in parens after the primary build number but, at least in iWeb Buddy, I have removed it from the display entirely. Since it is no longer a sequential number it would only potentially confuse users and it displays in the crash reports anyway.
The updated script is as follows:
# Xcode auto-versioning script for Subversion by Axel Andersson
# Updated for git by Marcus S. Zarra and Matt Long
use strict;
# Get the current git commit hash and use it to set the CFBundleVersion value
my $REV = `/opt/local/bin/git show --abbrev-commit | grep "^commit"`;
my $INFO = "$ENV{BUILT_PRODUCTS_DIR}/$ENV{WRAPPER_NAME}/Contents/Info.plist";
my $version = $REV;
if( $version =~ /^commit\s+([^.]+)\.\.\.$/ )
{
$version = $1;
}
else
{
$version = undef;
}
die "$0: No Git revision found" unless $version;
open(FH, "$INFO") or die "$0: $INFO: $!";
my $info = join("", );
close(FH);
$info =~ s/([\t ]+CFBundleVersion<\/key>\n[\t ]+).*?(<\/string>)/$1$version$2/;
open(FH, ">$INFO") or die "$0: $INFO: $!";
print FH $info;
close(FH);
Since git is distributed, there is no need to be online to produce a build. The script will grab the current abbrev-commit hash and will inject it into the current build’s Info.plist file.
you may want to give ‘git rev-parse HEAD’ a try. it’ll give you just the sha1 without the need for all of that scary regex magic :).
Nice script. I use Mercurial instead of git, so I hacked your script to use the revision number and 12-char changeset hash mercurial uses. It gets the job done. Thanks.
# Xcode auto-versioning script for Subversion by Axel Andersson
# Updated for git by Marcus S. Zarra and Matt Long
# Updated for Mercurial by Daniel Lord
use strict;
# Get the current Mercurial revision number and 12-char truncated changeset hash
# and use it to set the CFBundleVersion value e.g. 0:b1f34247e734
my $REV =`/usr/local/bin/hg log -r tip | grep “^changeset”`;
my $INFO = “$ENV{BUILT_PRODUCTS_DIR}/$ENV{WRAPPER_NAME}/Contents/Info.plist”;
my $version = $REV;
if ( $version =~ /^changeset:\s*([0-9]+[:]{1}[0-9a-z]+)$/ )
{
$version = $1;
}
else
{
$version = undef;
}
die “$0: No Mercurial changeset found” unless $version;
open(FH, “$INFO”) or die “$0: $INFO: $!”;
my $info = join(“”, );
close(FH);
$info =~ s/([\t ]+CFBundleVersion\n[\t ]+).*?()/$1$version$2/;
open(FH, “>$INFO”) or die “$0: $INFO: $!”;
print FH $info;
close(FH);
Using the command suggested by kevinoneill, a slightly less reg-ex’y version :)
import os
from Foundation import NSMutableDictionary
version = os.popen4(“/opt/local/bin/git rev-parse HEAD”)[1].read()
info = os.environ[‘BUILT_PRODUCTS_DIR’]+”/”+os.environ[‘WRAPPER_NAME’]+”/Contents/Info.plist”
plist = NSMutableDictionary.dictionaryWithContentsOfFile_(info)
plist[‘CFBundleVersion’] = version
plist.writeToFile_atomically_(info, 1)
And, of course, the PyObjC version for Mercurial for fans of Hg like me:
#!/usr/bin/env python
import os
from Foundation import NSMutableDictionary
version = os.popen4(“/usr/local/bin/hg log -r tip | grep ‘^changeset’ | sed s/changeset://”)[1].read()
info = os.environ[‘BUILT_PRODUCTS_DIR’]+”/”+os.environ[‘WRAPPER_NAME’]+”/Contents/Info.plist”
plist = NSMutableDictionary.dictionaryWithContentsOfFile_(info)
plist[‘CFBundleVersion’] = version
plist.writeToFile_atomically_(info, 1)
Thanks for this script. I looked a bit around and find the following command which will give you the number of commits into a git branch: “git rev-list HEAD | wc -l”
I ve put this into a small ruby script which will use this number and set it as the build number.
#!/usr/bin/ruby
require “rubygems”
require “Plist”
pl = Plist::parse_xml(ENV[‘BUILT_PRODUCTS_DIR’] + “/” + ENV[‘WRAPPER_NAME’] + “/Contents/Info.plist”)
pl[“CFBundleVersion”] = `/usr/local/git/bin/git rev-list HEAD | wc -l`.to_i.to_s
pl.save_plist(ENV[‘BUILT_PRODUCTS_DIR’] + “/” + ENV[‘WRAPPER_NAME’] + “/Contents/Info.plist”)
Please be aware that you have to have the Plist gem installed and that my git is in another directory when somebody wants to use it.
Thanks for posting your script!
I found that ‘git-describe –long’ gives a perfect format for typical versioning:
my $git_description = qx{ git-describe –long };
my ( $release, $patch, $commit ) = ( $git_description =~ /([^-]+)-(\d+)-g(\w+)/ );
$release .= “.$patch” if $patch;
print “Version $release (Build: $commit)\n”;
You can then tag your releases as 1.0, 2.6 etc, and the above lines will produce something like:
Version 1.0.5 (Build: 7abc23f)
I also added the following commands to the top of the script to automatically commit the current state, which then automatically bumps up the patch number… sweet!
use POSIX;
my $timestamp = strftime( “[%Y-%m-%d %H:%M:%S %z]”, localtime() );
system( qw{ git-add . } );
system( “git-commit”, “-a”, “-m”, “$timestamp automatic build commit” );
So with these changes, and an appropriate run-script build rule, every build can be re-created at the drop of a hat.
Problem solved.
Thanks!
(yeah there are ways to mess this up by using git-rebase etc, but this follows my development “happy path” perfectly)
git rev-parse –short HEAD will give you the shorter unique sha-1.
Also, rather than {WRAPPER_NAME} and the rest, you could use {INFOPLIST_PATH} instead.