Automated Xcode Build Numbers, Late 2016 Edition

I really shouldn't care so much about build numbers. But I do; I want to avoid thinking about them as much as possible day-to-day.

My desires are simple:

  1. Their calculation should be automated. I prefer using the git commit count (not perfect logic, I'd love the commit hash, but good enough). This keeps it ever-increasing and numeric, automatically.
  2. With said automation this should keep build numbers in sync across all targets automatically. Our iOS apps are getting tons of targets thanks to extensions and watchOS apps, and I don't want to manually remember to increment my sticker pack up to build 2033 or whatever before I upload.
  3. I don't want anything to do with build numbers mucking up my git history. This means: no post-commit hook to automatically update Info.plist. "Build 0" or some other invalid value should live in git, with the real number calculated at build or archive time. Why? Last thing I want are silly merge conflicts because of build numbers in Info.plists. They're calculated values, why persist them and dirty the history?
  4. In this multi-target world, I might choose to launch my Watch App in the sim instead of the host app. So I can't have the build number script only in the host app target.
  5. The only thing I want in source control is the version number. That's fine, I don't want to automate that. (For those interested: I have a project-level setting for that and Info.plist for each target references ${VERSION_NUMBER}, that way I only need to maintain it in one location.)
  6. I don't want to use the Info.plist preprocessor .h file. The less I have to git-ignore, the better. I did that for a few years and it always ended up annoying me at some point.

I've had a pretty good solution working throughout the years, despite some wrenches I had to fix when Apple gave us watchOS 1.0. It largely mirrored Jared's solution. Basically: dynamically update the Info.plist with a calculated number in the temporary build directory. That way you never modified the actual files under source control. Clever, and worked.

Then Xcode 8 hit.

Running on device worked great, the first time. If you updated some code and hit run again you'd see this:

Turns out, Xcode 8 changes some things regarding it's signing / validation, and mucking about with the Info.plist after Copy Bundle Resources happens causes a signature error. It should notice the change in Info.plist and recalculate the signature, but it doesn't.

The reported way to fix this error? Do a clean. Between every build. A thread has started on the dev forums that's a bit more specific to the Info.plist cause of this error. They suggest using another file as an intermediary (Xcode will notice it changing, it only seems blind to Info.plist), and copying out of that into Info.plist after Copy Bundle Resources. Ick.

After a day of playing around, I gave up on the idea of mucking about with Info.plist files in the build directory. I'll skip right to the solution I arrived at:

  1. In every target, as a first step in the build process, update the source-controlled Info.plist with a build number.
  2. At the end of each targets' build phases have a second script run to reset the build number in Info.plist to the fake value, in my case 0.

Things I don't like:

  • If I cancel mid-build the Info.plist files will stay dirty. I've gotta remember to let a build finish before I commit. I usually don't check in after a failed or canceled build, though :P
  • Basing it on git commit count means multi-branch life can yield duplicate build numbers. But once you've merged back onto the main or develop branches it all syncs up (this was a problem of the old way, too)
  • I need those 2 steps added to every target (my old method required 1 script per target, so not much of a loss really).

But, it works great so far. It also gets around Jared's need to update the Info.plist in the dSYM too -- all that gets propagated automatically.

I wish there was a way to script that cleanup to trigger anytime I ran a plain old clean, just for a quick "I need to check this in and don't want to build", but I can't see a way to do that. I don't anticipate that happening often, and I can just elect to not stage the files anyway.

Hope this helps some other people out.

Copy-pastable scripts for those interested:

Set Build Number

git=`sh /etc/profile; which git`  
branch_name=`$git symbolic-ref HEAD | sed -e 's,.*/\\(.*\\),\\1,'`  
git_count=`$git rev-list $branch_name |wc -l | sed 's/^ *//;s/ *$//'`

/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $git_count" "${PRODUCT_SETTINGS_PATH}"

echo "Updated build number in ${PRODUCT_SETTINGS_PATH} to $git_count"  

Clear Build Number

/usr/libexec/PlistBuddy -c "Set :CFBundleVersion 0" "${PRODUCT_SETTINGS_PATH}"
echo "Cleared build number in ${PRODUCT_SETTINGS_PATH}"