Automated build numbers in Xcode

Build numbers are one of those things that we just have to deal with while writing software, but it usually provides little benefit to end-users. Much like a git commit hash, it's best left to the computers to generate for us. Lets talk about automating build numbers out of our lives.

Before digging into the how, lets define what I'm trying to achieve though automation:

  • I want the version number (1.5.1) to be manually updated as I see fit as it is largely a marketing aspect, not a technical one. I'll usually do this on the release branch when I start a new version.
  • I want the build number (1086) to be set automatically based on the number of commits, on the current branch in git, at the time of the build.
  • I want the build number to include the branch name (1086-secret-feature-x) if we aren't building a binary for the App Store.
  • I want the build numbers to be updated without any affect on my source control.

That last one is a bugger.

My old way - Using an Info.plist preprocessor

A few years ago I invested a little time automating build numbers for my apps in Xcode. Like many I stumbled upon this post from Cocoa is my Girlfriend that leverages info.plist preprocessing and git.

His method takes advantage of being able to define variables in a header file which Xcode will use to automatically replace values in your info.plist file. Just add a build script to keep the value in that header file up-to-date based on the number of git commits and you have automated build numbers. Pretty simple to get working.

Where it falls apart is how that header file affects your source control checkins. Every build will dirty the header. If you don't want to dirty your git repo with checkins on this file you'd need to do something like git update-index --assume-unchanged Source/InfoPlist.h or add it to the .gitignore to make sure it isn't ever checked in (after you check in a dummy file the first time so people who checkout the repo don't get errors), but that falls apart quickly if you're switching branches a lot.

(I've been trying git-flow in Tower recently, which makes heavy usage of branches)

You also run into issues with caching as Xcode tries to cache your info.plist file in between builds and doesn't always re-trigger the pre-processing. This can be a little frustrating when you'd archive without remembering to do a clean first.

My new way - getting dirty with PlistBuddy

If we can't rely on Xcode to update the plist file for us we'll have to do it ourselves. Here is the script in its entirety:

git=`sh /etc/profile; which git`
branch_name=`$git symbolic-ref HEAD | sed -e 's,.*/\\(.*\\),\\1,'`
git_count=`$git rev-list $branch_name |wc -l | sed 's/^ *//;s/ *$//'`
simple_branch_name=`$git rev-parse --abbrev-ref HEAD`

build_number="$git_count"
if [ $CONFIGURATION != "Release" ]; then
	build_number+="-$simple_branch_name"
fi

plist="${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
dsym_plist="${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist"

/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "$plist"
if [ -f "$DSYM_INFO_PLIST" ] ; then
	/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "$dsym_plist"
fi

This makes all the needed build number changes in the build directory itself, so there is no affect on your source-controlled files.

One item of note: it was important to not forget the dSYM info.plist in a script like this (dSYM is the file that stores the debug symbols for your app to help you track down crashes). Leaving that update out will cause HockeyApp, for one, to complain at the mis-match between the build number in the app and the build number in the debug symbols. I'm not sure if this mis-match would affect crash logs in iTunes Connect, too, I never let it get that far.

How to install this script

Xcode screenshot

  • Select your project at the top of the navigator on the left
  • Select the target you'd like to enable automatic build numbers for
  • Select build phases
  • From the menu select Editor -> Add Build Phase -> Add Run Script Build Phase
  • Copy and paste the above script
  • Drag the build phase you added to re-order it to be just after the Copy Bundle Resources step

For good measure I'll usually go into where you normally define the build number (under General for the target) and set it to "UKNOWN." I've never had the script fail, but I'm always paranoid and want to see something obviously wrong if it does.

And, by the way, this can work for any executable target type except Watchkit Apps. This means you can synchronize your build numbers across your app target and any extensions you may have.

I'm looking for a way to easily synchronize version numbers across targets, haven't gotten there yet.