Sharing Cocoa

Learn from my mistakes.

Automatic Version Number from Git

I wrote this piece a while ago in my notebook, that I keep in TiddlyWiki, but listening to Jose's great series on Git in 85%Cocoa (in Spanish), I decided to share it with you in this blogger site that I had created some time ago and never really used. If you like it, I will try to publish more.

I use git for the version control of my projects and although it isn't news that Xcode 4 will come with Git integration, while Xcode 3.2 is still the required version for submitting apps to the App Store, I do want to enjoy the benefits of automatic version number generation in my projects. If you do to, hope this helps. Daniel Jailcut and Markus Zarra had previous versions that helped me to write this (Thanks guys!). Read those too.

iPhone and Mac OS X version numbers are stored in the plist of the project. There are two variables in that file that control the version number of an application:

  • CFBundleShortVersionString specifies the release version number of the bundle, which identifies a released iteration of the application. The release version number is a string comprised of three period-separated integers. The first integer represents major revisions to the application, such as revisions that implement new features or major changes. The second integer denotes revisions that implement less prominent features. The third integer represents maintenance releases. This is the marketing version.
  • CFBundleVersion identifies an iteration (released or unreleased) of the application. This is the build number.

I would like to create a script to be integrated as an XCode target that does automatic updating of this two fields of the info.plist file.

Git commit numbers are non sequential. Actually, they are hashes of the committed content. It is necessary to have a sequential number for the build number if you want to use this method both for iPhone and Mac OS X projects. In particular, both the App Store and the Sparkle framework (and probably the Mac OS X, although I haven't tried yet) require this sequential number in order to work properly.

Solution

The sequential number that is required can be maintained in a file that is not controlled by Git, so that branching or reverting changes does not change back the number. The build phase script will update the value automatically every time a build is successful. If this happens every time the project is built, successfully or not, the number would grow very fast although the numbers between successful builds are not needed.

When branches are used, which is one of the strengths of Git, a higher build number can be assigned to a patch of a current version than to the yet to release newest version. In order to add some sequence even in those cases, I will precede the sequential build number with the marketing version (1.0.0, 1.1.2, or 2.0.0 for example). In this way it is clear that 1.1.0.139 is not newer than 2.0.0.127, although the build number is.

The marketing version will only be modified if any commit has been tagged, containing the marketing version number.

In order to obtain a clean git status, build_number should be added to the .gitignore file.

Additionally the file with the build number could be copied to another file number that is tracked with Git, so that reverting to a previous commit contains information about the build number that was used at the time of the commit.

Implementation

I have created a perl script that does this using the PerlObjCBridge to read and write the plist, reads the build number from the file, and pipes to communicate with git to use describe --tags (that provides the last tag assigned).

The script is:

#!/usr/bin/perl
# gitversion.pl
#
# Created by Jorge D. Ortiz Fuentes on 09/11/09.
# Copyright 2009 PoWWaU. All rights reserved.

use Foundation;
use File::Copy;

# Script customizable variables
$git="/usr/local/git/bin/git";

$script_action = $ENV{'ACTION'};
$info_plist = $ENV{'INFOPLIST_FILE'};
#$project_dir = $ENV{'PROJECT_DIR'};
#$project_name = $ENV{'PROJECT_NAME'};
#$src_root = $ENV{'SRC_ROOT'};

if (script_action == "build") {
    print "Working with project properties: $info_plist\n";
    unless (-e "$info_plist.old") {
        copy($info_plist, "$info_plist.old") or die "Copy failed: $!";
    }

    # Read the info.plist file
    $project_plist = NSMutableDictionary->dictionaryWithContentsOfFile_($info_plist);

    if (!$project_plist or !$$project_plist) {
        die "One of the following failed: $plist - $$plist\n";
    } else {
        # Obtain the last tag to set the marketing version (CF
        if (open(GIT_TAG, "$git describe --tags HEAD |")) {
            $marketing_version = ;
            chomp($marketing_version);
            close(GIT_TAG);
            print "Marketing version:|$marketing_version|\n";

            $project_plist->setObject_forKey_(NSString->stringWithCString_($marketing_version),
                'CFBundleShortVersionString');
        } else {
            print "Failed to obtain git describe: $!.\n";
            print "Not updating CFBundleShortVersionString\n";
        }

        # Obtain the CFBundleVersion (or build number)
        # For non sequential numbers use: git rev-parse --short HEAD.
        open(GIT_BUILD, "build_number") || die "Failed to obtain git rev-parse: $!.";

        $build_number = <git_build>;

        chomp($build_number);
        close(GIT_BUILD);

        $bundle_version="$marketing_version.$build_number";
        print "Build version: |$bundle_version|\n";

        $project_plist->setObject_forKey_(NSString->stringWithCString_($bundle_version),
            'CFBundleVersion');

        # Write the updated plist.
        $project_plist->writeToFile_atomically_($info_plist, "1");
    }
} else {
    print "Doing NOTHING. ACTION isn't 'build'";
    exit(1);
}

Integration

Setting the information into the info.plist

  1. Create a new build phase for the current target that consists in running an script.
  2. Add the following script to that build phase:
  3. run_script="gitversion.pl"
    echo "Running a custom build phase script: $run_script"
    "$PROJECT_DIR/Scripts/$run_script"
    script_exit_status=$?
    echo "Finished running custom build phase script: $run_script (exit
    status = $script_exit_status)"
    exit "$script_exit_status"
    
  4. Copy the perl script into a new project directory named Scripts with the name gitversion.pl.
  5. Add the script file to the project ensuring that it is NOT added to any of the targets of the project.
  6. Move the build phase to the beginning in order to ensure that the info.plist is modified before copying it to the bundle.

Incrementing the build number

  1. The first time the project is created, initialize a file with the build number set to 1 in the main folder of the project.
    echo "1" > build_number
  2. Create a new build phase that consists on running a script.
  3. Add the following script to that build phase:
    echo "Incrementing sequential build number."
    # copy the previous build number for book keeping in Git
    cp build_number build_number.prev
    # and increment it
    bn=$(cat build_number)
    (( bn=$bn+1 ))
    echo $bn > build_number
    echo "Build number updated to $bn. Finished."
    
  4. This phase must be the last one for the target.

Additionally if I want to avoid incrementing the build number for non-successful builds, there is a setting in the build section of the Xcode preferences to "Continue building after errors" that can be disabled.