Automating your build numbers

There are two keys in your project’s .plist file that allow you to identify your application’s build, wether it’s an Apple submission, an ad-hoc, a development or enterprise build. Good management of these keys can simplify your development workflow specially when it comes to submitting builds to iTunesConnect and getting debug information from users and test devices.

Chances are that you already know about these plist keys and are familiar with using them:

  • Bundle version, also known as CFBundleShortVersionString 
  • Bundle versions string, short, also known as CFBundleVersion

Normally you would use the CFBundleVersion key as the displayable version of the app, users would normally be able to look up in your application’s about, launch screen or settings page. You would normally manage this number manually based on team and product decisions, and that’s ok. This bundle version normally ends up looking like this:

  • 1.0
  • 1.2
  • 1.2.4

On the other hand, you can use CFBundleShortVersionString as an internal build representation or, alternatively, as a complement of CFBundleVersion. I like to use it as an incremental number that uniquely identifies every build of the app.

  • 1
  • 2
  • 3 …

The thing is, you don’t want to manually change this number every time you make a build. That’s a lot of upkeep for something so simple. This is a perfect example of where the rule:

“If you’re repeating it more than once it can probably be automated…”

…applies. Now how would you go about doing this? Well, it comes out that the solution is really simple. All you have to do is go into your project’s Build Phases, add a Run Script, make sure the script runs at the top of your stack and then select a language!

A new Run Script phase comes with multiple sections, of which we will only care about the first two:

screen-shot-2017-02-20-at-10-19-19

  • Shell: defines the language to use for the script.
  • The shell contents. Which, as you can see, you can type in directly or drag from a separate file your project folder.

Great! Now let’s edit this Build Phase:

The Unix way (old school)

Set the Shell value to:

/bin/sh

This will allow us to read or type in unix commands. Now in the body type in:

buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}”)
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}”

This scripts grabs the current build number from your project, increases the build number by one and then sets it back into your project using the PlistBuddy tool. It looks fairly cryptic as most Unix commands do but the logic is quite straightforward.

If you build your project now you will notice that the build number will increment every time you make a new build on the project. This is a great first step, but you should ask yourself if you really want that number to increment every time you make a build. I’ll give you two reasons why you don’t want this behaviour:

  • A natural app’s development cycle goes through hundreds, or even thousands of iterations. You don’t want to be sending version 1.0 of your app to apple using build number 1024 and version 2.0 of your app using build number 1048576.
  • Code merge conflicts will always occur if you’re working with a team, since everyone will be editing and changing this number unconsciously as they develop.

What you really want is to make this build smarter and only increment the build when you’re compiling your App for an ad-hoc or release build. Fortunately, the code you need to add for this is minimal:

if [ ${CONFIGURATION} == "Release" ]; then
  buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}”)
  buildNumber=$(($buildNumber + 1))
  /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}”
fi

 

The added if statements checks against your build configurations, making sure that you are actually running a release build before updating the build number. If you’ve added any other build configurations to the project, you might want to append this new build configuration check to the if statement using an || operator. i.e:

if [ ${CONFIGURATION} == "Release" ] || [ ${CONFIGURATION} == "OtherConfiguration" ]; then
  buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}”)
  buildNumber=$(($buildNumber + 1))
  /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}”
fi

 

And that’s it! Now we have automated build version updates using 5 lines of code. Great job. Time to give yourself a pat on the back! 🙂


But… Hold on a second there! What about us newbies that don’t want the hassle of unix and like to work with swift?!

Well…

You’re in luck! You can also use a newer, trendier and more hipster friendly approach:

The Swift way

Set the Shell value to:

/usr/bin/env xcrun --sdk macosx swift

This will set the language of your script to swift. Now before we go into the Swift code, it’s worth mentioning that the swift approach works differently to the Unix approach. Unlike Unix, environmental libraries don’t just live in your runtime, you’ll have to fetch them from your process and parse them using a dictionary. The other difference as opposed to the Unix script is that you have to exit the script with one of two codes: 0 for success and 1 for failure.  This all may seem a bit convoluted at first but once you get the hang of it you’ll realise the possibilities are endless!

import Foundation

//we fetch the environment variables and configuration from our process info.
let enviromentVariables = ProcessInfo.processInfo.environment
guard let configuration = enviromentVariables["CONFIGURATION"] else {
  exit(1)
}

//check that the configuration is release
if configuration == "Release" {

  //fetch the project and info plist path variables
  guard let rootPath = enviromentVariables["SRCROOT"],
        let plistPath = enviromentVariables["INFOPLIST_FILE"] else {
    exit(1)
  }

  //read the version string from the plist file
  var url = URL(fileURLWithPath: rootPath)
  url.appendPathComponent(plistPath)
  guard let dictionary = NSMutableDictionary(contentsOf: url),
        let versionString = dictionary["CFBundleVersion"] as? String,
        let versionNumber = Int(versionString) else {
    exit(1)
  }

  //update the incremented value of CFBundleVersion and update the plist file
  dictionary["CFBundleVersion"] = "\(versionNumber + 1)"
  dictionary.write(to: url, atomically: true)
}

//exit the program without any failures
exit(0)

 

And there you have it! This code will work exactly the same as the previous Unix code, with all the added advantages that working in a Swift environment provides. The main difference here is that you have to take a defensive, step by step approach in order to execute your commands.

One more thing…

The examples showed here are a solution geared towards simple projects with default build configurations. Some projects, however, might have multiple targets and require more elaborate work that the scripts shown here. These scripts serve an example on how you can automate build processes using two of the most predominant languages in Apple environments. Feel free to play around and adapt these to suit your needs!

Author: Danny Bravo

Director @ EPIC