Versioning Mac OS X Applications Best Practicies
Versioning a Mac OS X application is pretty easy. There are just two keys in the Info.plist
that need to be set. But there are some rules and guidelines (some well documented, and some not) that you should follow to provide the best experience for your users. I will go over these, as well as some tools and techniques to make the process less painful.
The two keys that need to be set are CFBundleVersion
and CFBundleShortVersionString
, and are described in the Runtime Configuration Guidelines. CFBundleShortVersionString
is also called the “marketing version” and is presented to the user. It’s what gets shown in Finder’s Info Panel, but is not interpreted by the system. It can be pretty much anything you want. CFBundleVersion
is called the “build version”. It is actually used by the system, so it is important you follow the rules. Personally, I don’t see much of an advantage to having separate build and marketing version numbers. In my ideal world, they would be the same, and I’d just set the two keys to the same value. However, fate is against me. Let’s start out by looking at the official definition of CFBundleVersion
.
Quoting the Runtime Configuration Guidelines:
This key specifies the exact build version of the bundle. This string is usually of the form nn.n.nxnnn where n is a digit and x is a character from the set [abdf]. The first number is the major version number of the bundle and can contain one or two digits to represent a number in the range 0-99. The second and third numbers are minor revision numbers and must be a single numeric digit. The fourth set of digits is the specific build number for the release.
Since it’s not described in that quote, the [abdf] character is known as the build stage: development, alpha, beta, and final candidate (or release candidate). The nn.n.nxnnn format seems to have some odd restrictions. For example the version 1.1.10 is illegal. It turns out, this is an artifact from the pre-OS X days, and is all described in Technote TN1132: Version Territory. In OS X with Launch Services, this is no longer the case. How Launch Services interprets the build number is not really documented, however, so I asked on the carbon-dev mailing list. I received the following response from an Apple employee:
I can tell you that when comparing version numbers for document binding purposes, Launch Services respects the first three version components, but IGNORES the development stage code (a, b, d, etc.) and anything following it.
...
Effectively, LS expects the following format: nnnnn[.nn[.nn]][X] where n is a digit 0-9, square brackets indicate optional components, and X is any string not starting with a digit. X is ignored when present.
On one hand this is good, since we no longer have the single digit restrictions for the minor and bug revisions. On the other hand, we have some new restrictions since the development stage code is ignored. This is problematic if you release public beta versions and include the development stage in your build version. There are a few techniques to create LS-compatible build versions, though.
All of Apple’s released apps have a floating point build version that is completely unrelated to the marketing version. Take a look at Safari as an example. On OS X 10.4.7, it has a marketing version of 2.0.4, and a build version of 419.3. By doing this, you can have the build number increase between beta and released versions. Apple has support for this technique in its build tools, and is known as the “apple-generic versioning” scheme. Chris Hanson provides a nice article explaining how to use Xcode and agvtool
.
Another technique is to use the Subversion revision number as the build version. But even though it sounds nice and simple, it can have some strange side effects. First, it is not unreasonable for a revision number to get into the hundreds of thousands, especially if you have multiple projects in the same repository. Launch Services only looks at the first 5 digits, so you could run into some strange behavior for large revisions. More seriously, the revision number is always increasing. While this is usually what you want out of a build version, it can backfire. Say you released versions 1.1.0, 1.1.1, and then jump right to 2.0.0. Now, after the release of 2.0.0, you discover a critical bug in the 1.1.x series. You create a branch, and release 1.1.2. The problem is version 1.1.2 now has a later Subversion revision than 2.0.0, and thus a higher build version. Launch Services will think 1.1.2 is newer than 2.0.0, which is not what you want.
So with Subversion revision numbers ruled out, you should really use apple-generic versioning. Personally, I don’t like apple-generic versioning much, either, namely agvtool
. I don’t want some tool mucking with my Xcode project file and the Info.plist
. Xcode provides cleaner ways to deal with both these issues. With build variable substitution, you can set CFBundleVersion
in the Info.plist
files to ${CURRENT_PROJECT_VERSION}
. It will get filled in with the value of the build variable when it gets copied into the application bundle. You can create a custom build variable for the marketing version, too, say CURRENT_MARKETING_VERSION
, and apply the same technique for CFBundleShortVersionString
. Thus, there’s no reason use agvtool
to update Info.plist
.
You can also use Xcode configuration files to keep the version variables in a separate text file. In Xcode, create a new configuration file, and call it version.xcconfig
. Put CURRENT_PROJECT_VERSION
and CURRENT_MARKETING_VERSION
in there, and have your project based on version.xcconfig
. Now, you can easily update the version numbers with a simple text editor or shell script.
The final advantage apple-generic versioning gets you is the two global variables it creates. From agvtool(1):
A versioned target will have two global variables generated and linked into your product. One is of type double and is simply the CURRENT_PROJECT_VERSION. The other is a version string which is formatted to be compatible with what(1). These variables are available for use in your code.
So now we see why build versions are required to be floating point numbers. If you’re comfortable with this restriction, go ahead and use apple-generic versioning. Even if you use build variable substitution and Xcode configuration files, you can still enable apple-generic versioning, and not use agvtool
.
But since this is a pretty minor advantage, you may want to forgo apple-generic versioning, and roll your own. I’m not much of a fan of the constantly increasing floating point number. It seems ambiguous when you are supposed to increase the decimal number vs. the whole number. And what happens when you release 1.1.1 and then 2.0.0? Do you add 100, so that you have room for more 1.1.x versions if you need to? This all seems to be a lot of voodoo, and I don’t like voodoo.
As an alternative, I think I’ve devised an easy way to map marketing versions to LS-compatible build versions. Remember, LS recognizes the format nnnnn.nn.nn. If you restrict each revision number in your marketing version to be from 0 to 99, you can encode the “major.minor.bug stage release”, to a format of “MM.mm.bb s rr”. You can assign each development stage a number, say 0 for development, 1 for alpha, 2 for beta, 3 for release candidate, and 9 for released. To encode this as LS-compatible, you re-arrange the decimal points: “MMmmb.bs.rr”. Thus a marketing version of “1.3.10b4” has an encoded build version of “1031.02.04”. And “2.0.1” has an encoded version of “02000.19.00”.
So, this isn’t a huge advantage over AGV, and you do have to restrict your revisions to less than 100, but I think I am going to try this out on my future applications to see how it will work.