zargony.com

#![desc = "Random thoughts of a software engineer"]

Automatic versioning in Xcode with git-describe

Do you manually set a new version number in Xcode every time you release a new version of your app? Or do you use some tool that updates the Info.plist in your project like agvtool or PlistBuddy? Either way, you probably know that it's a pain to keep track of the version number in the project.

I recently spent some time trying out various methods to automatically get the version number from git and put it into the app that Xcode builds. I found that most of them have drawbacks, but in the end I found a way that I finally like most. Here's how.

Getting a version number from git

Why should we choose a version number manually if we're using git to manage all source files anyway. Git is a source code management tool that keeps track of every change and is able to uniquely identify every snapshot of the source by its commit ids. The most obvious idea would be to simply use the commit ids as the version number of your software, but unfortunately (because of the distributed nature of git) commit ids are not very useful to the human reader: you can't tell at once which one is earlier and which is later.

But git has a very useful command called git describe that extracts a human readable version number from the repository. If you check out a specific tagged revision of your code, git describe will print the tag's name. If you check out any another commit, it will go back the commit history to find the latest tag and print its name followed by the number of commits and the current commit id. This is incredible useful to exactly describe the currently checked out version (hence the name of this command).

If you additionally use the --dirty option, git describe will append the string '-dirty' if your working directory isn't clean (i.e. you have uncommitted changes). Perfect!

So if tag all releases of your app (which you should be doing already anyway), it's easy to automatically create a version number with git describe --dirty for any commit, even between releases (e.g. for betas).

Here are some examples of version numbers:

v1.0                   // the release version tagged 'v1.0'
v1.0-8-g1234567        // 8 commits after release v1.0, at commit id 1234567
v1.0-8-g1234567-dirty  // same as above but with unspecified local changes (dirty workdir)

Automatically set the version number in Xcode

You'll find several ideas how to use automaticly generated version numbers in Xcode projects if you search the net. However most of them have drawbacks that I'd like to avoid. Some ways use a custom build phase to create a header file containing the version number. Besides that this approach gets more complicated with Swift, it only allows you to display the version number in your app, but doesn't set it in the app's Info.plist. Most libraries like crash reporters or analytics will take the version number from Info.plist, so it's useful to have the correct version number in there.

So let's modify the Info.plist using PlistBuddy in a custom build phase. But we don't want to modify the source Info.plist, because that would change a checked-in file and lead to a dirty workdir. We need to modify the Info.plist inside the target build directory (after the ProcessInfoPlistFile build rule ran).

Instructions

  • Add a new run script build phase with the below script.
  • Make sure it runs late during building by moving it to the bottom.
  • Make sure that the list of input files and output files is empty and that "run script only when installing" is turned off.
# This script sets version information in the Info.plist of a target to the version
# as returned by 'git describe'.
# Info: http://zargony.com/2014/08/10/automatic-versioning-in-xcode-with-git-describe
set -e
VERSION=`git describe --dirty |sed -e "s/^[^0-9]*//"`
echo "Updating Info.plist version to: ${VERSION}"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${VERSION}" -c "Set :CFBundleShortVersionString ${VERSION}" "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
/usr/bin/plutil -convert ${INFOPLIST_OUTPUT_FORMAT}1 "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${VERSION}" -c "Set :CFBundleShortVersionString ${VERSION}" "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist"

Thoughts

  • By keeping the list of output files empty, Xcode runs the script every time (otherwise it would detect an existing file and skip running the script even if changes were made and the version number may have changed)
  • Some sed magic strips any leading non-numbers from the version string so that you can use tags like release-1.0 or v1.5.
  • PlistBuddy converts the plist to XML, so we're running plutil at the end to convert it back to the desired output format (binary by default)
  • If you need more information than just the output of git describe, try the excellent "autorevision" script.

Update: A second call to PlistBuddy updates the version information inside the dSYM bundle to match the application version

Update 2: Locate the dSYM bundle using DWARF_DSYM_FOLDER_PATH since the dSYM bundle isn't always created in the TARGET_BUILD_DIR (e.g. when archiving).