Because I'm forgetful, I wanted to automated this process. Basically, I wanted to increment the version short string any time we do an Archive and increment the bundle build ID any time we do a Release configuration build, but leave the version numbers alone on Debug builds.
Unfortunately, several of our projects are ones that we inherited or took over, so not every project uses the same version numbering scheme. How we increment 1.0b5 is different from how we increment 1.0.12, or a simple build number like 1058.
The way I deal with this is a Run Script Build Phase in my application's executable target that runs the following Ruby script (make sure you set the "shell" field to /usr/bin/ruby, and make sure the script is the last build phase in the application). Feel free to use this script if you wish and modify it to meet your needs. If you improve it, I'd be glad to incorporate improvements back into it. One item of note: the way that I differentiate between Archive builds and other Release configuration builds might be a bit fragile since I'm relying on an undocumented naming pattern in an environment variable.
Note: I'm aware of agvtool. I avoided it for two reasons. First, I wanted more control over the numbering schemes, and second, I tried using agvtool in a build script a few years back, but at that time, there were issues when you bumped the version numbers of a project that was currently open. Those issues may have been resolved, but I didn't want to fight that battle again.
data = ''
f = File.open(filename, "r")
f.each_line do |line|
data += line
end
return data
end
parts = old_value.split(letter)
version_num = parts[0]
alpha_num = parts[1].to_i
alpha_num = alpha_num + 1
new_version = version_num.to_s + letter + alpha_num.to_s
print "Assigning new version: " + new_version + "\n"
new_key = "<string></string>"
part_1 = infoplist[0, start_of_value - '<string>'.length];
part_2 = new_key
part_3 = infoplist[end_of_value + "</string>".length, infoplist.length - (end_of_value - start_of_value + (new_key.length - 1))]
new_info_plist = part_1 + part_2 + part_3
new_info_plist
end
start_of_key = infoplist.index(key)
start_of_value = infoplist.index("<string>", start_of_key) + "<string>".length
end_of_value = infoplist.index("</string>", start_of_value)
old_value = infoplist[start_of_value, end_of_value - start_of_value]
print "Old version for " + key + ": " + old_value + "\n"
print old_value.class.to_s + "\n"
old_value_int = old_value.to_i
print old_value_int.class.to_s + "\n"
if (old_value.index("a") != nil) # alpha
infoplist = handle_alpha_beta(old_value, "a", infoplist, start_of_value, end_of_value)
elsif (old_value.index("b") != nil) # beta
infoplist = handle_alpha_beta(old_value, "b", infoplist, start_of_value, end_of_value)
elsif (old_value.index(".") != nil) # release dot version
parts = old_value.split(".")
last_part = parts.last.to_i
last_part = last_part + 1
parts.delete(parts.last)
new_version = ""
first = true
parts.each do |one_part|
if (first)
first = false
else
new_version = new_version + "."
end
new_version = new_version + one_part
end
new_version = new_version.to_s + "." + last_part.to_s
print "New version: " + new_version.to_s + "\n"
new_key = "<string></string>"
infoplist = ""
elsif (old_value.to_i != nil) # straight integer build number
new_version = old_value.to_i + 1
print "New version: " + new_version.to_s + "\n"
new_key = "<string></string>"
part_1 = infoplist[0, start_of_value - '<string>'.length]
part_2 = new_key
part_3 = infoplist[end_of_value + "</string>".length, infoplist.length - (end_of_value+1)]
infoplist = part_1 + part_2 + part_3
end
infoplist
end
config = .upcase
config_build_dir =
archive_action = false
if (config_build_dir.include?("ArchiveIntermediates"))
archive_action = true
end
print "Archive: " + archive_action.to_s + "\n"
print config
if (config == "RELEASE")
print " incrementing build numbers\n"
project_dir =
infoplist_file =
plist_filename = "/"
infoplist = get_file_as_string(plist_filename)
infoplist = find_and_increment_version_number_with_key("CFBundleVersion", infoplist)
if (archive_action)
infoplist = find_and_increment_version_number_with_key("CFBundleShortVersionString", infoplist)
end
File.open(plist_filename, 'w') {|f| f.write(infoplist) }
else
print " not incrementing build numbers"
end
7 comments:
Was about to implement one myself, for the same reason! THANK YOU!
Thanks for that. Spent a few hours testing the Apple-generic(agvtool) and this. This obviously offers more flexibility.
I customized it so the ShortVersionString gets incremented every time I build a release (rarely) and the Version gets incremented no matter whether it's for Release or Debug.
Jeff,
This is awesome. I use TestFlight as well and build numbers have become a bane of mine. This script is exactly what I was looking for!
P.S.
I love your blog and have gotten a ton of use from it. If your looking for a volunteer to help maintain a github repo with your code snips and scripts please consider me!
Jeff, as always, your posts are interesting and useful. I appreciate the effort you put into your blog. As you probably know there are a gazillion articles/approaches about auto-incrementing build numbers in iOS projects. But given the hight quality of your writing, I thought I'd start with your approach.
I have a question, a comment and a bug fix.
Bug fix first: Version numbers with matching parts (e.g. 1.1, 2.2.2, 3.0.3) get mangled (becoming .2, .3, 0.4 respectively). The problem lies in the line: parts.delete(parts.last). Replacing it with parts = parts[0, parts.length - 1] should do the trick.
The comment: one problem with the current way of dealing with configurations is that in Xcode 4, Profile builds use the Release configuration. So whenever you exercise your code under Instruments, you'll bump your version number. I'm still trying to wrap my head around Xcode 4 Schemes, so I'm not yet sure what a good solution is.
Now for the question. This is a minor point, but I want to verify whether I understand the build process. In a sense, this script only affects the next build, not the current one. Looking at the logs I see that the run script build phase occurs after ProcessInfoPlistFile. Is there any reason you wouldn't want to run this script as a pre-build action (again, Xcode 4 Schemes)?
Matt:
I'm honestly a little confused about why
parts.delete(parts.last)
and
parts = parts[0, parts.length - 1]
would do different things, but appreciate the fix. My Ruby is a bit rusty, not having done anything of any complexity in three or four years, but those seem like they say to do the same thing...
Yes, the internal build number, but not the marketing build number, gets incremented when you do a non-Archive release build. That's the functionality we wanted here, but you're welcome to change it.
And yes, this bumps the number AFTER building, which (again) is the functionality we wanted for MartianCraft. You can move the build-phase earlier in the process if you want to bump the number before doing the build. I think this is a six-of-one-half-dozen-of-the-other situation. I prefer knowing that my next build will have the number in my Info.plist now. Fortunately, it's simple enough to change if you prefer it the other way.
Jeff
Jeff,
Sorry if this is a dupe, but it's been a couple of days so it's possible I only imagined sending this:
parts.delete(parts.last) is not deleting based on index. It deletes all array entries equal to the entry in the last slot of parts. In other words it works like removeObject: on NSMutableArray, rather than what you want: removeObjectAtIndex:
By and large I agree with your preference about relating the build to the current number in the plist. Mostly I was wondering about the build pre-action because I'm trying to understand schemes in Xcode 4.
A very neat way of incrementing build number. I was thinking about integration with a versioning system, i.e. a tag on a SVN repository and let this tag be the same as your build number
Post a Comment