Experimentation with build numbering

Build numbering is not the most interesting subject but during my investigations I found some cool ideas to use in my own scheme.

##tl;dr

MacRuby is cool and I over engineered a solution for fun.

#Starting point

I started out with some StackOverflow questions 1, 2 and then ended up with this blog post over at cimgf.com. I’ve not used pearl and as stated in some of the comments it’s got some hairy regex’s in there which ideally I’d rather not have to deal with.

I’m a Ruby fan and as such that would be my preferred language to use for this. Scrolling down to the comments on that blog there are examples in python and ruby so there’s a good starting off point.

##Issue 1

The example in the blog uses the commit hash as the build number.

In accordance with Apple’s Information Property List Key Reference the commit hash can not be used as the build number because this this property should be:

… a monotonically increased string, comprised of one or more period-separated integers

A couple of examples on StackOverflow and in comments of the original blog post suggest using

git rev-list HEAD | wc -l

This first command git rev-list HEAD prints the hash of each commit on a new line

paul$ git rev-list HEAD
ced64f89f841d46b2a4262a2987ca363be704d49
ff538caa32f704fb09295d41a20fe2f8488ca6ae
72847eb10e3c3731a696c84e3ae4179640d28ed2

This is then piped into wc -l which counts the number of lines given to it, in this case from standard input. Here the result would be 3.

I quite like this idea for numbering as it works a lot better when the code base is shared among a team. If you was simply incrementing a counter you have to deal with issues such as where is it stored and how do you keep it consistent. It should also serve as a rough guide to how many stable builds it took to get the app to it’s current state.

##Issue 2

I quite like the thought of having the commit hash

I’d like to keep the commit hash details so that I can easily track down what exact commit this came from. A quick scan of the Apple’s Information Property List Key Reference confirms that you are allowed to add any keys/values to the info plist.

##Additional requirements

#Let’s build

So I started out by wrapping up those build paths that come from the ENV variable

##Helpers

class Environment
  %w(BUILT_PRODUCTS_DIR INFOPLIST_PATH WRAPPER_NAME).each { |const| const_set(const, const) }
  
  def initialize env
    @env = env
  end
  
  def build_info_plist_path
    "#{@env[BUILT_PRODUCTS_DIR]}/#{@env[INFOPLIST_PATH]}"
  end
  
  def build_settings_plist_path
    "#{@env[BUILT_PRODUCTS_DIR]}/#{@env[WRAPPER_NAME]}/Settings.bundle/Root.plist"
  end
end

I only need two paths but they need to be constructed and I would rather not have this construction logic scattered about. I’m also preferring to use constants for keys as opposed to strings so I’m dynamically generating them first.

This next bit may look especially odd for people who don’t know Ruby, but do know Objective-C

class NSMutableDictionary
  def self.plist_open path
    plist = self.alloc.initWithContentsOfFile path
    if block_given?
      yield plist
      plist.writeToFile path, atomically: true
    end
    plist
  end
end

The cat is out of the bag - I’m playing around with MacRuby. I can get the native handling of plists from Objective-C but the ease of Ruby. Here I am opening up the NSMutableDictionary class and adding my own helper method to open a file, allow me to make changes and then save them back out. I added this as a convenience for later on.

##Actual logic

The constructor for the main class looks like this

class XcodeVersioner  
  %w(DefaultValue PreferenceSpecifiers CFBundleShortVersionString CFBundleVersion PSGitCommit Key).each { |const| const_set(const, const) }
  
  attr_reader :bundle_version, :build_number, :git_commit, :env
  
  def initialize env
    @env            = env
    @build_number   = `git rev-list HEAD | wc -l`.to_s.strip
    @git_commit     = `git rev-parse HEAD`.to_s.strip
    @bundle_version = ''
  end

...
  

I start out by declaring constants again to avoid magic strings being littered everywhere. Then I add the attr_reader’s for ivars. The Environment helper class is passed into the constructor and then the basic information is retrieved from git.

Next up is the method that gets called to start the real work

def run
  version_info_plist
  version_settings
  puts "Bundle Version => #{bundle_version} (#{build_number}) with commit #{git_commit}"
end

In this method we just call two more methods where the nitty gritty details are hidden and then print out the result (this will show up in the build logs).

If you’ve not used blocks much in Objective-C then you really should. You can make some really cool API’s, of course this is Ruby but they work pretty much the same. Using the helper method I added to NSMutableDictionary I get something like this for editing the info plist

def version_info_plist
  NSMutableDictionary.plist_open(env.build_info_plist_path) do |plist|
    @bundle_version        = plist[CFBundleShortVersionString]
    plist[CFBundleVersion] = build_number
    plist[PSGitCommit]    = git_commit
  end
end

Let’s break this one down line by line

  1. Method defintion
  2. Call the helper method with a path to the file. The NSMutableDictionary opens the file and then yield’s it back into the block as the variable plist
  3. I grab the bundle version from the info plist file and store it into an ivar
  4. I set the CFBundleVersion version to the build_number retrieved in the constructor
  5. I set the PSGitCommit to the git_commit retrieved in the constructor
  6. The end of the block
  7. The end of the method

The code between the do and matching end are the block of code that gets yield‘d to by the plist_open method.

The method for working with the settings bundle is very similar in concept, but it’s slightly more awkward to work with due to the structure of the plist.

The structure of the settings bundle plist looks like this

{
  "PreferenceSpecifiers" => [
    {"Type"=>"PSGroupSpecifier", "FooterText"=>"Some Footer Text", "Title"=>"Info"}, 
    {"DefaultValue"=>"", "Key"=>"CFBundleShortVersionString", "Type"=>"PSTitleValueSpecifier", "Title"=>"Version"},
    {"DefaultValue"=>"", "Key"=>"PSGitCommit", "Type"=>"PSTitleValueSpecifier", "Title"=>"Debug"},
  ], 
  "StringsTable"=>"Root"
}

The bit’s we care about are the dictionaries that are held within an array. The array is contained in a dictionary under the key PreferenceSpecifiers.

What this means for me is that I have to loop through each item in the array and then use a case statement to figure out if it is a preference specifier that I want to edit. This all ends up looking like:

def version_settings
  NSMutableDictionary.plist_open(env.build_settings_plist_path) do |plist|
    plist[PreferenceSpecifiers].each do |preference_specifier| 
      case preference_specifier[Key]
      when CFBundleShortVersionString
        preference_specifier[DefaultValue] = "#{bundle_version} (#{build_number})"
      when PSGitCommit
        preference_specifier[DefaultValue] = git_commit[0...10]
      end
    end
  end
end

##Conclusion

This probably looks completely over engineered when compared to the original blog post’s code. I would have to agree with that statement, but I do believe that I’ve had some fun experimentation with MacRuby, made a useful tool for myself and have abstracted the logic out nicely for easier maintenance. Before I did this I never thought of the power at my fingertips with the Cocoa framework and Ruby combined - this example is only taking advantage of NSMutableDictionary’s ability to natively handle the plist format but I am sure better examples could be found.