Wednesday, April 1, 2009

Automated Commit and Build Number Incrementing

For some projects, I like to commit and increment my build number every time I compile. Not every project, but on some. In other cases, I only want to automatically commit and increment the build number when I do a Release configuration build.

Apple provides a tool called agvtool which stands for Apple Generic Versioning Tool. But, you generally don't want to use it while you have an Xcode project open, so incorporating it into a run script build phase is not advised.

I've found a few examples on the internet of scripts that incorporate the Subversion build number, but they didn't quite fit my needs. The ones I found were written in Perl and Python, and I'm not particularly comfortable in either of those languages, so I decided to write my own in Ruby, with which I have a much higher comfort level, rather than tweak those existing ones to meet my needs.

This script does not utilize the versioning system built-into Xcode, but rather directly sets the Bundle Version based on the Subversion version number. It also does a commit if there have been any changes since the last build before grabbing the version number from Subversion.

To use this script, right-click on your Target where you want to use this and select Add->New Build Phase->New Run Script Build Phase.

When the window opens up, set the Shell to
/usr/bin/ruby
and then paste the following script into the window if you want to commit and increment on every build:

def get_file_as_string(filename)
data = ''
f = File.open(filename, "r")
f.each_line do |line|
data += line
end
return data
end

# commit if any changes
%x[svn -m 'automated build commit' commit]

svn_version = %x[svnversion -n]
parts = svn_version.split(":")
new_version = parts[1]

# Figure out where the Info.plist file is
project_dir = ENV['PROJECT_DIR']
infoplist_file = ENV['INFOPLIST_FILE']
plist_filename = "#{project_dir}/#{infoplist_file}"
infoplist = get_file_as_string("/Users/jeff/dev/FlyPaper/Resources/Info.plist")
start_of_key = infoplist.index("CFBundleVersion")
start_of_value = infoplist.index("<string>", start_of_key)
end_of_value = infoplist.index("</string>", start_of_value) + "</string>".length
old_value = infoplist[start_of_value, end_of_value - start_of_value]

new_key = "<string>#{new_version}</string>"
new_info_plist = "#{infoplist[0, start_of_value]}#{new_key}\n#{infoplist[end_of_value+1, infoplist.length - (end_of_value+1)]}"

File.open(plist_filename, 'w') {|f| f.write(new_info_plist) }


or use this script if you only want to commit and increment the build number on builds using the Release configuration:

def get_file_as_string(filename)
data = ''
f = File.open(filename, "r")
f.each_line do |line|
data += line
end
return data
end

if ENV['BUILD_STYLE'] == "Release"
# commit if any changes
%x[svn -m 'automated build commit' commit]
end

svn_version = %x[svnversion -n]
parts = svn_version.split(":")
new_version = parts[1]

# Figure out where the Info.plist file is
project_dir = ENV['PROJECT_DIR']
infoplist_file = ENV['INFOPLIST_FILE']
plist_filename = "#{project_dir}/#{infoplist_file}"
infoplist = get_file_as_string("/Users/jeff/dev/FlyPaper/Resources/Info.plist")
start_of_key = infoplist.index("CFBundleVersion")
start_of_value = infoplist.index("<string>", start_of_key)
end_of_value = infoplist.index("</string>", start_of_value) + "</string>".length
old_value = infoplist[start_of_value, end_of_value - start_of_value]

new_key = "<string>#{new_version}</string>"
new_info_plist = "#{infoplist[0, start_of_value]}#{new_key}\n#{infoplist[end_of_value+1, infoplist.length - (end_of_value+1)]}"

File.open(plist_filename, 'w') {|f| f.write(new_info_plist) }


After you paste it in, close the window. You might want to rename the run script build phase to something more meaningful. You can do that by right-clicking on it and selecting Rename from the menu that pops up. Also, you need to drag the new build phase up so that it fires before the Copy Bundle Resources phase so that the commit happens before it builds the application. Otherwise, the application will always reflect the previous version number in the Get Info window.

That's all you have to do. Now, whenever you build or do a Release build, Xcode will commit your files to the Subversion repository if there have been changes, then set the bundle version to the value currently in Subversion. Now, this script has not been extensively tested. If you have problems, let me know about them, and I will try to fix any issues that arise.



14 comments:

Bart said...

Automatically doing a commit sounds funny to me, unless you are the only developer. I mean, what would the commit message be? What if it broke stuff? It's not even tested. Good post otherwise!!

Jeff LaMarche said...

Bart:

That's why I said "some" of my projects. Many of my projects are single-developer projects, and the commit message is "automatic build commit".

The benefit is - the build number gets compiled into the app. If a tester gives me the build number from their application's about box, I can get back to the exact code that was used to compile the application they're using.

You're right, though, it doesn't lend itself well to team projects.

Jeff LaMarche said...

I have added a second version of the script that only commits on Release builds. That's probably more appropriate for multi-developer teams.

YoNoSoyTu said...

I agree with Bart that a automatic commit sounds like a bad idea (even if you are working alone).

But a couple of tips: I use a similar approach for my projects, and I have found a couple of problems with your script. You say to include a Run Script build phase to your target, but the problem is that any build phase happens after Info.plist file has been already processed by Xcode, so your revision number will be always a revision behind (or you have to build twice). This is obviously prone to errors.

My solution is include another target, which my main target depends on, so it will be run before Xcode uses the Info.plist file. This target is simply a Run Script build phase like the one you describe. To be more clear: "Main Target" depends on "Update Build Number Main Target" which contains a "Run Script" build phase.

I also keep my script to updating the build number in a external file (so I can use it in several targets), and in each "Update Build Number *" target I pass some arguments into the script (the project dir and the path to the Info.plist file). In my script I also use Ruby OSX/Cocoa classes to read the Info.plist file, which makes my script a lot easier to read (a less prone to errors on my side).

I have left my script and my build phase at http://gist.github.com/89109 . Hope your readers find it interesting.

Fabien Penso said...

Jeff,

I'm not sure the index method is appropriate here.

Did not test the following but you get the idea :

# Required :
# gem install plist

current_plist_file = "#{ENV['PROJECT_DIR']}/#{ENV['INFOPLIST_FILE']}"
current_plist_hash = Plist::parse_xml(current_plist_file)

current_plist_hash['CFBundleVersion'] = %x[svnversion -n].split(':')[1]

File.open(current_plist_file, 'w') {|f| f.write(current_plist_hash.to_plist) }


or in pastie : http://pastie.org/434818

Jeff LaMarche said...

YoNoSoyTu

Anyway, you say that the build phase happens after the Info.plist file has been processed. I'm not finding that to be the case. I just went, made some changes, compiled and ran a project that uses this, and the application has the correct version number in the about box. Did you move the run script build phase so that it fired before the compile and build phases?

Fabien:

I'll have to try yours out, it's more elegant. I've been away from regular expressions for a little while and did what felt natural (mine's actually a very NSString-like solution, neh?). I'm not sure that it's "inappropriate", but a good regular expression is always better than brute force. :)

That being said, I didn't want to rely on any gems for this, which is why I manually write the file rather than going through the plist gem.

PJ Cabrera said...

Hi Jeff,

Just wanted to point out you hard coded a path into both examples. Line 20 of the first example, and line 23 of the second one.

infoplist = get_file_as_string("/Users/jeff/dev/FlyPaper/Resources/Info.plist")


Not all of your readers will catch the problem right away. You might want to change that in the post to :

infoplist = get_file_as_string(plist_filename)

supagroova said...

Hi,

To add to the community clean-up here, I think you can ditch the get_file_as_string() function and just use:

infoplist = File.read(plist_filename)

:-)

Fabien Penso said...

Jeff,

You do lots here, but no regex at all. Here is something that uses regex :

http://pastie.org/435158

YoNoSoyTu said...

Jeff,

Yes, the "Run Script" was my first build phase of the target, and in the detailed view of the build output the Info.plist file was processed by Xcode before the "Run Script". I am talking here about iPhone projects, maybe standard Cocoa projects do not have this limitation.

Evan Robinson said...

I wrote a quick little command-line tool using xcode that reads your plist file (provided as the only command line argument to the tool), finds CFBundleVersion, increments it, and writes your plist file back out. This can be used in a build script without requiring reload of your xcode project.

Here's the code:
#import <Foundation/Foundation.h>

#define kCFBundleVersionKey @"CFBundleVersion"

int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

NSString *filename = [[NSString alloc] initWithFormat:@"%s", argv[1]];
NSDictionary *infoDictionary = [NSDictionary dictionaryWithContentsOfFile:filename];
NSMutableDictionary *newDictionary = [infoDictionary mutableCopyWithZone:NULL];

int cfBundleVersion = [(NSNumber *) [newDictionary valueForKey:kCFBundleVersionKey] intValue];
++cfBundleVersion;
NSLog(@"new CFBundleVersion:%d", cfBundleVersion);
[newDictionary setValue:[NSNumber numberWithInt:cfBundleVersion] forKey:kCFBundleVersionKey];

[newDictionary writeToFile:filename atomically:YES];

[pool drain];
return 0;
}

It's very crude, but it does the job so far. I apologize for the lack of formatting, but blogger doesn't allow code tags

cirrostratus said...

After trying four different solutions in shell script, python and ruby, Evan Robinson's Obj-C-based command line script finally worked for me, with one modification. His script was writing the bundle version as an integer which caused the dSYM process to fail when building my iPhone 3.0 app in Xcode 3.2. Here is the mod I had to make to get things running smoothly:

[newDictionary setValue:[[NSNumber numberWithInt:cfBundleVersion] stringValue] forKey:kCFBundleVersionKey];

Edwin said...

scrub m65 kamagra attorney lawyer body scrub field jacket lovegra marijuana attorney injury lawyer

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels