Using Build and Version Numbers and the Art of Injecting Them into Your App

Prompted by a recent article by generous Jeff LaMarche, I thought I’d post my process for generating build numbers and having them automatically written into in my builds so they can be displayed in an About view like this:

Deep Green Version Information

The principle I’m using is to write the version information to the generated app bundle in the build process in such a way that it doesn’t change anything in the source tree. This is because I want to avoid the situation where I have to make a new commit to the source repository after I’ve tagged and built the released version.

The shell script I’m using is very simple, runs as a build phase and uses agvtool and git under the hood. Contrary to Jeff, I don’t have any issues using agvtool, which integrates really well with Xcode.

Using the script to automatically generate version and build numbers in your app only really takes three steps:

  1. Download the script and store it within your source code tree.
  2. Add a call to the script as part of your build process.
  3. Bump build number and tag commits when desired.

Step 1 and 2 are done just once per Xcode project.

I’ve only run it in my own setup. It may not work for you. If it doesn’t, please let me know.

What Are Build Numbers Anyway?

Build numbers are unique references to specific builds, or versions, of an application. Where version numbers typically come in the form of two to three parts (the major and minor feature version numbers and possibly a bug fix version number), build numbers are sequential integers, typically incremented at each build that leaves the developer’s machine.

As I bring up the version information of TextMate, where I type this, it reads 1.5.10 (1631). The build number is 1631, the version number is 1.5.10. End-users are normally mostly concerned with the version number. agvtool calls this the marketing version and the build number just the version.

Subversion, being a centralized revision control system, maintains a globally unique revision number on the repository which can be used as the build number for a build of any particular snapshot of the repository. Git, on the other hand, is a distributed version control and therefore can’t compute a globally unique sequential number. Instead Git uses a SHA to identify a specific commit snapshot which isn’t very human readable.

Since I use Git, thank you very much, I’m relying on my simple script to manage the build and version numbers for me. The build number is tracked in the project itself (managed via agvtool), and the version number is managed using Git tags.

How to Use Build Numbers and When to Bump’em

Even though build numbers are “just numbers”, I prefer to decide when to generate a new one. In the process of developing an app I find it easiest for everybody involved to refer to these unique build numbers instead of a version number which may or may not change at every internal release. And the build number of each released version should be incremented by only one at a time so that everybody knows they haven’t missed a version somewhere in the process.

In other words, I want to increment the build number when I release a version, for either test or App Store, not just because I’m making a new commit (already uniquely identified by its SHA) or happen to compile using a certain configuration (as suggested in Jeff’s article).

The main reason I don’t want to automatically bump the build number each time I compile using a certain configuration is that it sometimes takes a few build attempts to get it right (most often because of the Provisioning Hell).

The Workflow Around a Release

In my daily life as an Cocoa developer, I distinguish between three types of builds. These types are also mirrored by the three branches in the Git repository:

  1. Development builds take place all the time during the normal development workflow of coding, building and running. Code changes are committed to the development branch, and the build number is not incremented in this process (however, the version number will look different after each commit, see below).

  2. Test builds are intended for a larger audience, i.e. testers and other stakeholders, who’ll run the build on their registered devices. These builds are compiled from the test branch and tagged with the version number after the build number has been manually incremented.

  3. App Store builds immediately follow a successfully tested Test build. App Store builds only differ from Test builds in that they’re committed on the master branch and submitted to the App Store.

For more information on the repository layout, see my (very casual) talks on creating a smooth development workflow using Git and GitHub:

Each time I distribute a Test build, I go through the following process which introduces my custom version.sh script:

  1. Check out test and merge development into it (--no-ff).
  2. Update the change log, targeted testers, describing new functionality, changes and bug fixes in the release.
  3. Bump the build number: version.sh bump. This is basically the the same as running agvtool bump -all, but my script just generates a less noisy output.
  4. Commit the changes locally: git commit -a. Don’t push to a remote repository just yet as you may run into compilation quirks and need to fix those first.
  5. Tag the commit with the version number, e.g.: git tag -a v1.2.
  6. Archive (also builds) the target, using a distribution configuration, either from within Xcode or the command line with xcodebuild.
  7. Distribute the binary (you’re using TestFlight or similar, right?)
  8. Merge the test branch back into development (--no-ff).

You can leave out the test branch roundtrip (step 1 and 8) if you don’t care about having your Test releases on a separate branch.

Note that all of the above could be fairly easily automated, depending on how you do your change log update and distribution. I prefer to do it by hand because… I guess I just like to see this succeed little by little.

The key to the process here is the manually triggered build number update and tagging the commit which version.sh uses for setting the version number. And note that the only place we’re keeping track of and storing the build number is in the project itself.

Automating the Version Number Injection

Let me finally get to the issue I wanted to talk about in the first place: how to get the version number into your app.

version.sh does more than just bump the build number. It can also write the version number to the compiled app bundle — i.e. into the generated Info.plist file. If you add a Run Script Build Phase after compiling the target dependencies and before copying the bundle resources, for example, version.sh can write the version numbers to the generated app bundle like this:

Xcode Build Phase Script

Make sure to have version.sh stored on a path included in our $PATH environment variable or use the project-relative path to the script. In my case, it typically looks like this:

Work/Scripts/version.sh --set --plist-path \
  "${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}" --quiet

It’s worth noting that this will alter the Info.plist of the built app, not the Info.plist in your source tree. What this means is that your source tree won’t get dirty from this operation, and you won’t have to either reset or make another commit which would create a mismatch between the code you tagged and the code containing the correct version number. In other words, the version number is only stored in the Git tag. Only the build number is stored in the project as mentioned.

Reading the Version Number from Within Your App

Getting out the version information from the app bundle is very straightforward too. Just call the following:

NSDictionary *appInfo = [[NSBundle mainBundle] infoDictionary];
NSString *versionStr = [NSString stringWithFormat:@"%@ (%@)", 
    [appInfo objectForKey:@"CFBundleShortVersionString"], 
    [appInfo objectForKey:@"CFBundleVersion"]];

This will set the version string to something like @"1.2 (224)" as shown in the screenshot above.

More on the Script

It’s less than 160 lines of code, most of which is boilerplate, but I won’t list the script here. It’s hosted on GitHub in case you want to have a look at it. If you run version.sh --help from the command line, you’ll get the following output:

version.sh 1.0 by Joachim Bondo <osteslag@gmail.com>

Manages build and version numbers using git and agvtool.

Usage:
  version.sh [options]

Options:
      --terse              Prints the version number in the terse format
  -s, --set                Sets the version number using agvtool
  -p, --plist-path <path>  Write to the built Info.plist file directly
  -b, --bump               Bumps the release number
  -q, --quiet              Supresses output
  -h, --help               Displays this help text
  -v, --version            Displays script version number

Notes:
  - If no options are given, the current versions are printed
  - Specifying -s requires a --plist-path and uses the git tag

You should run the script from your project directory — you’ll get an error message if you don’t. Conveniently it’s is the default for running build phase scripts.

I use it mainly to set build and version numbers and write them to the generated app bundle as described above, but you can also use it to read out the current values, by simply invoking the script without parameters:

$ version.sh
Version: v1.2d6 (42)

Again, the build number is read from the project, the version number is derived from the Git tag by calling git describe --tags. When setting the version number in the app bundle, the script will remove any leading 'v' character.

A nice thing about using the Git tag is that if you make a development build, in-between any Git tagging, you’ll get a version number like v1.2d5-54-gbacc9a8. In this case the version is built on a commit identified by the shown (partial) SHA which is 54 commits after the last tagged version, v1.2d5. This works really well for Development builds.

Outtro

Despite this rather long post, I hope to have demonstrated how to use this simple script to easily generate and inject build and version numbers into your iOS and Mac OS X apps.

  1. osteslag posted this
blog comments powered by Disqus