Versioning with Xcode

Posted 8 years, 38 weeks ago.

A large part of creating software is versioning. That in mind, versioning with Xcode has always been — and continues to be — a pain. Now that iOS supports frameworks and extensions, it’s only getting worse. Though undocumented, it’s suggested that all of your targets have the same build and version numbers. It doesn’t stop things working, but you will get a warning when you submit to the App Store. So off you go, trundling through your targets, updating your version number so everything matches. Not ideal, really.

Well, this is inconvenient.

The Apple Generic Versioning Tool

Over the course of my experience in building iOS apps, I’ve looked into ways of easing this pain. Perhaps the least obvious is Apple’s built in versioning tool, agvtool. I honestly hadn’t even heard about it until recently, but it turns out it’s quite handy. It’s part of the command-line tools package that you install alongside Xcode, so if you haven’t got that already, I’d suggest starting there.

Setting it up requires a little configuring in your Build Settings, firstly setting VERSIONING_SYSTEM to “Apple Generic”, which tells it that you’re going to use agvtool. Then you set CURRENT_PROJECT_VERSION to match whatever your main target’s current build number is (i.e. CFBundleVersion). That’s really it. Now you can use agvtool.

One of the caveats of agvtool is that you need to close your Xcode project first. This is because it updates your project file, and could potentially cause problems for Xcode. In my tests I’ve found Xcode 6.4 seems to handle it pretty gracefully, but don’t count on that. Best to close your project before continuing.

In your terminal app of choice, navigate to the folder containing your project and from there you can run agvtool in a few ways. The first is to set your marketing version, which is usually something like “1.2”. This is the version that people see on the App Store, and likely the version you’ll use when talking about the release. So let’s say we’re working on 1.3, and we want to update our project to match. To do that, use the command:

agvtool new-marketing-version 1.2

You’ll see that the tool updates all of your versions to what you’ve provided, across all of your targets. You can do much the same thing with your build number (the CFBundleVersion from before). To apply a given build number, use:

agvtool new-version -all 8504

Again, this will update everything: the project file, all your Info.plist files across your targets. But you can also just bump the version to the next one.

agvtool next-version -all

This command determines the current version number from the project file and then increments it to the next highest integer (so floats get rounded up). Again, it happens across all of your targets, which means no more errors like before. AWESOME.

Automating Build Numbers

You, like me, might be wincing right now. Sure, agvtool is super nice for ensuring all your version numbers are matching across your targets, but couldn’t we do something with this that runs when we build?

Well, yes and no. The problem is that agvtool updates your project file, so while using it to increment your build number as part of your actual build works, it cancels the build because Xcode thinks something has changed. Not super ideal, but don’t lose hope just yet.

PlistBuddy is another command-line tool that comes as part of the same toolkit, which lets you read and update values in a plist with a simple command. There are a whole slew of scripts out there which use this for incrementing build numbers as part of the build cycle, and until recently, this was my latest incarnation:

status=$(git status --porcelain)
plist=${INFOPLIST_FILE}
if [[ "${#status}" != 0 ]] && [[ $status != *"M ${plist}"* ]]; then
    buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${plist}")
    buildNumber=$(($buildNumber + 1))
    /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${plist}"
fi

What it’s doing is not difficult. It gets the current status from git, and then check to see if any changes have been made. If they have, and the target’s Info.plist hasn’t been updated already, it increments the build number and updates the plist.

To implement it, you select your project in the project navigator. Then head to the build phases tab, and add a “Run Script” build phase to your main target using the plus button in the top left of the list. Drop in the script, and it should look something like this:

Run, Script, Run!

But actually, now that I’ve begun adding extensions to my project, I’m finding it lacking. The line where I set the new build number has to be repeated multiple times for each new target I add. The marketing version isn’t replicated across the board either, which means I still have to manually change each of my targets (or, more specifically, I have to remember to).

The Best of Both Worlds

So ideally we would use something that can automatically increment our build numbers when we’ve madee changes, and ensure that both they and our marketing number get replicated across all of our targets. We’d also like it to happen when we actually build, rather than needing to do it manually.

Anyone who listens to Mobile Couch knows that I’m a sucker for building stuff myself. So that’s what I did. I created a script which I can call that does all of the above things, and best of all, can be used in a build phase.

To use it, first download the script from the Gist and put it into your project folder somewhere (mine sits in the project root). This ensures that everyone working on the project can use the script, which is important, given that we’re using a run phase to automate it.

Oh, and the run phase? It’s a whole lot simpler.

sh "update-version.sh"

The script reads the main Info.plist file and increments the version when git says there have been changes made, just like my previous script. But if that’s not your thing, a slightly different version will set the value based on the number of commits in the HEAD branch, and it’ll also append the current branch name for non-master branches, just to ensure no conflicts creep up on you.

sh "update-version.sh" --reflect-commits

Both versions will output a few lines about what they’re doing so that you can see what’s going on as a part of your build status.

A little feedback is a glorious thing.

There’s some settings you can apply within the script to ensure maximum compatibility with your project… or just to tweak because you can, but that’s all there is to it. Automatic versioning management that doesn’t require you to leave Xcode.

Now I can finish shipping this damn GIFwrapped release.