A sensible way to increment bundle version (CFBundleVersion) in Xcode

tl;dr: Automatically set your app’s bundle version based on the number of git commits up to that point. Skip to the code!

The Problem
When I started working with Feathr over a year ago, one of the first issues I ran into was what to use as the bundle version (what I often refer to as the ‘build number’) in our apps. Originally, we were ignoring it until we had to change it – whenever it was time to submit a new version of an app, the new build number had to be higher than the previous submission. Pretty soon this got annoying, and I had the feeling we weren’t using the field to its full potential.

Research + Tactics
With a bit of Googling, I found a few ways people were automating changing the bundle version. Some people were auto-incrementing the field every time a build was run, but I might build my project a hundred times in a day if I’m trying to work out a small UI issue! That would quickly put my build number into the thousands, and if I continued to work on a project for any length of time it could become unnecessarily large. Plus, that number in itself would be useless to me, and I like my version numbers to mean something. Some SVN users put the current repository revision number as the build number… now that was something! The build number would point to something discrete – a specific revision of code – and wouldn’t change unnecessarily. If someone was having a problem with the app, all the developer needs is the build number – easily obtainable in tools like Crashlytics and easy to show somewhere in-app – to know the exact revision that the user is working with. This is a big win both during testing and when live apps are having issues.

The Snags
Alright, that is it, that is what I want to do. BUT WAIT… we use git for our source control. I won’t go into why I think git is superior to SVN (plenty of others have argued both sides of that), but the salient issue is that git, unlike SVN, does not give perfect integer revision numbers that increment on each commit. Instead, git hashes the commit data and uses that as a unique identifier of the commit. That means one commit could be 8d844d8dce7fbd0fe5b2bf8232ff1b82c8244648 and the next f1728db451a8a409215fa450d90d145ee77f9a4c, which is incomprehensible AND unusable as a bundle version.

The Compromise
Why not make a virtual “revision number” based on the number of commits that exist on a certain branch? Since I base all my work on a ‘development’ branch, I can simply have git count the number of commits on development and use that as my build number. I’m anchoring it to a specific branch so that the revision number is consistent: even if I check out another branch – to work on a new feature, for example – the number always points to the same commit and won’t differ between branches. This of course requires the branch used for counting to be long-lasting, and it’s only really useful if it’s where you distribute most of your builds from.

The Paradox (+ the fix)
One more problem: if you change the build number every time you add a commit (and you’re tracking the file that holds said build number in git), you’ll infinitely recurse into an update build number, commit updated-build-number, update build number again, commit again loop of horror. You could simply .gitignore the info.plist file, but that means you have to remove it from your repo (otherwise, modifications show even if it is .gitignored). To get around this (thanks to a suggestion I saw on StackOverflow1) we can leave the source-controlled info.plist file alone, and update the bundle version directly in the target build directory’s info.plist file. This means that the value of CFBundleVersion seen in Xcode is not used anywhere, since it is not overwritten until after info.plist is copied to the target build directory.

The Code

Usage

  1. Put this file in your repository (somewhere like /scripts/)
  2. In Xcode, create a Run Script build phase AFTER the Copy Bundle Resources phase
  3. In the Run Script, call path/to/update_build_number.sh [branch] where [branch] is the branch you want the count based on. If not supplied, master is used.

Explanation
If no branch is supplied (via command-line argument) then the script defaults to ‘master’. It uses git rev-list [branch] --count to get the number of commits on [branch], BUT it subtracts out the number of commits behind said branch that you are currently (with git rev-list HEAD..[branch] --count). This is so that if you aren’t at the tip of the branch (e.g. you’re checked out a few commits behind to find a problem), the build number will still be accurate to what you’re currently running. Also, instead of updating the info.plist file in your source directory, it modifies the info.plist in the target build directory; this way, you don’t have to check-in a constantly-modifying info.plist.

Wait a minute…
I know what you’re thinking: “Hey! You implied we’d be able to get back to a given build number’s code easily, but I can’t just git checkout [build number] to get to the [build number]th commit!”. This is true, however… There’s A Script For That™2. Just do nth-commit.sh build_number branch. Enjoy!


  1. SO answer that suggests editing the info.plist after it is copied to the target build directory 

  2. Modified from this gist 

13 thoughts on “A sensible way to increment bundle version (CFBundleVersion) in Xcode

  1. What’s more, Info.Plist in dSYM bundle should also be updated:

    /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}.dSYM/Contents/Info.plist"

     

  2. I tried just to copy the first codes into run script not using script file. But nothing changed on build number, always 1.

    Is is anything wrong I did?

    • Did you check the build number as it appears in the app (either by NSLogging it or displaying it in the app somewhere)? The build number that Xcode shows does not get updated.

    • Thanks! I’ve used this exact script in a few Xcode 6 projects without issue. What kind of output do you see in the build log during this step? (Sorry for the late response)

  3. This is absolutely brilliant. I’ve been looking for something like this ever since I moved from SVN to git. And the solution is blindingly obvious once you see it. (Once you see it…)

  4. First and foremost – THANK YOU. Of the many essays, dozens of Stack-overflow questions, and zillions of other entries I read and scanned – yours is the most sensible and reasonable solution to the problem. While you gracefully handle 2 things (* how to calculate a good ever-increasing version number using git, and ** how to apply it without dirtying your git work-directory), I have little more to the problem.

    I need also a preprocessor definition with that same version at COMPILE TIME. Now Apple’s “agvtool” is doing great work – alas – dirtying the project, info.plists, and regenerating a source-file with version number and version string symbols.

    But how to do it WITHOUT dirtying the work directory — I don’t know. Xcode build-steps are running at separate processes, and so they don’t share environment variables, and I find how one Xcode build-step (hmm, “Run Script Step”) can change the preprocessor definitions (macros) of later build steps.

    Do you have an idea?

    • You’re welcome! I’m glad you found my solution helpful.

      That’s an interesting question. Here are some thoughts:
      1. Does it really have to be a preprocessor definition? There are many arguments against using preprocessor definitions throughout your code, such as http://qualitycoding.org/preprocessor/ (that being said, I use them heavily for whitelabeling). Perhaps you could refactor and rely on runtime checks of the build number.
      2. Recently I needed to modify some source files at build time before they were copied into the bundle. I created two Run Script Steps, one that ran before the Copy Bundle Resources build phase and one that ran right after the Copy Bundle Resources build phase. The first script made backups of every file getting processed and then modified the files in-place, and the second script restored all of the backups over the modified files. This does technically dirty the working directory, but only during building — when the build is complete the working directory is clean again. You could use this pattern to make a Run Script that writes the build number into one of your preprocessor directives files using sed (or awk, etc) and then a second script that undoes that change.

      Good luck!

  5. One more – the “.dsym” addition breaks your script on my machine, claiming “file does not exist” (the info.plist file). I suspect I need to only apply the “.dsym” lines in “Release” builds? Or has Xcode 7 broken something?

    • I’m not super familiar with dsym files, but according to http://stackoverflow.com/a/22460268/627772 the dsym file only exists when the “Strip Debug Symbols During Copy” build setting is enabled (though I haven’t had this issue, and that setting is disabled on my debug builds). I’ve added a check to my script (see the updated gist) that should prevent the script from dying when there is no dsym Info.plist file.

Leave a Reply

Your email address will not be published. Required fields are marked *