Automatically set the version number in Xcode

There are a number of ways that this can be done but most of the ways I have seen outlined online are hacky and / or unreliable. So what we will be going over here is what I would consider to be best practices.

There are, broadly speaking, two places where one would want to use version information in an xcode project (be it a mac or iOS app or even a framework): the info.plist file (this mostly going to be user facing information) and in your program logic. Because these two types of output differ in some important ways the instructions here will concentrate on making separate outputs for each use case.

Instructions

  • Create a new Aggregate target.
  • Make your main target depend on it.
  • In your new target make a run script phase.
  • Make sure that the list of input files and output files is empty and that “run script only when installing” is turned off.

The following is an example script that makes the following assumptions: that the header you will use in your code will be in ./src/ and that Autorevision is installed.

#!/bin/sh

# This script drives autorevision so that you end up with two
# `autorevision.h` files; one for including in the program and one for
# populating values in the `info.plist` file (some of which are user
# visible).
# The cache file is generated in a repo and is read from
# unconditionally when building from a tarball.
# This script is meant to called from within Xcode, some of the
# variables are defined in the environment there.

# Config
export PATH=${PATH}:/sw/bin:/usr/local/bin:/usr/local/git/bin

# This header file uses a slightly different format and a customized
# cache suitable for use with an info.plist file.
infoPlistOutput="${DERIVED_FILE_DIR}/autorevision.h"
customCacheOutput="${OBJROOT}/autorevision.tmp"

# This is a header suitable for including is your code.
# The one that actually gets included is only updated when something
# changes to prevent needless rebuilding.
cHeaderOutput="${SRCROOT}/src/autorevision.h"
cHeaderTempOutput="${OBJROOT}/autorevision.h"

# This is what needs to be in a tarball to make things work.
cacheOutput="${SRCROOT}/autorevision.cache"



# Output the autorevision cache.
if ! autorevision -o "${cacheOutput}" -t sh; then
	exit ${?}
fi

###
# This section does some manipulations to make the output pretty for
# use in the info.plist

# Source the cache to allow for value manipulation.
. "${cacheOutput}"

if [ ! "${VCS_TICK}" = "0" ]; then
	# If we are not exactly on a tag make the branch look better and use the value for the tag too.
	N_VCS_BRANCH="$(echo "${VCS_BRANCH}" | sed -e 's:remotes/:remote/:' -e 's:master:Master:')"
	sed -e "s:${VCS_BRANCH}:${N_VCS_BRANCH}:" -e "s:${VCS_TAG}:${N_VCS_BRANCH}:" "${cacheOutput}" > "${customCacheOutput}"
else
	# When exactly on a tag make the value suitable for users.
	# The following tag prefix formats are recognized and striped:
	# v1.0 | v/1.0 = 1.0
	# The following tag suffix formats are transformed:
	# 1.0_beta6 = 1.0 Beta 6 || 1.0_rc6 = 1.0 RC 6
	N_VCS_TAG="$(echo "${VCS_TAG}" | sed -e 's:^v/::' -e 's:^v::' -e 's:_beta: Beta :' -e 's:_rc: RC :')"
	sed -e "s:${VCS_TAG}:${N_VCS_TAG}:" "${cacheOutput}" > "${customCacheOutput}"
fi

###

# Output for src/autorevision.h.
autorevision -f -o "${cacheOutput}" -t h > "${cHeaderTempOutput}"
if [ ! -f "${cHeaderOutput}" ] || ! cmp -s "${cHeaderTempOutput}" "${cHeaderOutput}"; then
	# Only copy `src/autorevision.h` in if there have been changes.
	cp -a "${cHeaderTempOutput}" "${cHeaderOutput}"
fi

# Output for info.plist prepossessing.
autorevision -f -o "${customCacheOutput}" -t xcode > "${infoPlistOutput}"

exit ${?}

As you can see autorevision.cache and src/autorevision.h will need to added to your vcs ignore list.

After that is done you will need to adjust the following settings in your main build target:

INFOPLIST_OTHER_PREPROCESSOR_FLAGS = -traditional
INFOPLIST_PREFIX_HEADER = $(DERIVED_FILE_DIR)/autorevision.h
INFOPLIST_PREPROCESS = YES

The last thing you will need to do is setup your info.plist file for the dynamic values.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleExecutable</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundleGetInfoString</key>
	<string>$(PRODUCT_NAME) version VCS_TAG :VCS_SHORT_HASH:, Copyright 2015 Contributors. See Acknowledgements for a list of contributors.</string>
	<key>CFBundleShortVersionString</key>
	<string>VCS_TAG :VCS_SHORT_HASH:</string>
	<key>CFBundleVersion</key>
	<string>VCS_NUM</string>
	<key>LSMinimumSystemVersion</key>
	<string>$(MACOSX_DEPLOYMENT_TARGET).0</string>
	<key>NSHumanReadableCopyright</key>
	<string>Copyright 2015 Contributors. See Acknowledgements for a list of contributors.</string>
	<key>VCSLongHash</key>
	<string>VCS_FULL_HASH</string>
	<key>VCSShortHash</key>
	<string>VCS_SHORT_HASH</string>
</dict>
</plist>

The reason for the CFBundleShortVersionString to be formatted as VCS_TAG :VCS_SHORT_HASH: is so that the short hash (or revision number for svn) shows up in your crash reports but not in a way that I have found chokes tools that works with them.

Wrapping up

This setup should work with minimum adjustment for any project in any vcs that autorevision supports and it should continue to work (with obvious caveats) even if you switch to another vcs down the line.

Written on March 16, 2015