2012-05-26

Autogenerated build information in Xcode 4

In the Xcode project settings there are "Version" and "Build" fields for each target. These are the CFBundleShortVersionString and CFBundleVersion fields respectively in the info plist. The Version field is the version identifier presented to the user, and is the one Apple cares about when submitting an app to the App Store. It should use a readable scheme like major.minor.micro, e.g. 1.0.2.

The Build field however can be whatever you want in order to identify a build: incrementing counter, revision, tag, etc. Many recipes for automatic versioning of an application target this field, but my requirements led to a slightly different solution. Those requirements were:

  • Don't touch the info plist. I want to update Version and Build manually.
  • Store extra build info in separate file that is automatically generated and isn't tracked in repo. I don't want to clutter the repo with frequent build info changes.
  • Include at least build date and revision instead of just build. Other possible data fields could be branch, tag, user, host, etc. This is necessary because a distributed development environment without a dedicated place to create builds has no way to synchronize build numbers. It is also impossible to know who created the build.

The first step to meet these requirements was to write a script, setversion.sh, which leverages the PlistBuddy tool to create and modify a plist:

#!/bin/bash

PLIST="$1"

HG=/usr/local/bin/hg
PLISTBUDDY=/usr/libexec/PlistBuddy

fields=(
    "BuildDate" "$(date -u +'%F %T')"
    "BuildRevision" "$($HG identify | cut -d ' ' -f1)"
)

# Create empty plist if it doesn't exist, clear it if it does
$PLISTBUDDY -x -c "Clear dict" "$PLIST"

# Add all fields to plist
for ((i=0; i < ${#fields[*]}; i=i+2))
do
    name=${fields[i]}
    value=${fields[i+1]}
    $PLISTBUDDY -x -c "Add :$name string \"$value\"" "$PLIST"
done

The script adds two fields. BuildDate is the UTC timestamp of the build, and BuildRevision is the short form revision hash from the Mercurial repo. This script is not added to the Xcode project but is kept in the repo (or somewhere more suitable for easy access and sharing).

Next we need to generate a version plist so that we can add it to the project. Run the script in a shell with a suitable plist path and name as argument.

./setversion.sh MyProject-Version.plist

Add the generated file to the Xcode project and make sure to mark the proper targets for it if you have more than one.

This file shouldn't be tracked in the repo so we add it to .hgignore:

*-Version.plist

Next we configure the script to run for each build. Select your project in Xcode and go to "Build Phases" for the target you want to add version information to. Select "Add Build Phase" and choose "Add Run Script". In the script text field add the path to the script with path to the version plist as argument.

$SRCROOT/$PROJECT_NAME/setversion.sh "$SRCROOT/$PROJECT_NAME/$PROJECT_NAME-Version.plist"

Rename the build phase if you like and drag it up below "Target Dependencies" to make sure it runs before files are compiled and copied.

That's it! Whenever a build is done in Xcode the version plist will be generated and can be easily accessed:

NSString *versionPath = [[NSBundle mainBundle] pathForResource:@"MyProject-Version" ofType:@"plist"];
NSDictionary *versionDict = [NSDictionary dictionaryWithContentsOfFile:versionPath];
NSString *buildDate = [versionDict objectForKey:@"BuildDate"];
NSString *revision = [versionDict objectForKey:@"BuildRevision"];