Dealing with project.pbxproj in Ruby
For Mac and iOS development the weapon of choice is Xcode. And it’s pretty good for what it does (if you disregard the current stability issues with Lion) when you are just making simple apps. In my own projects, however, things usually get more complex and I need to put a lot of stuff in static libraries and juggle these around in all the projects that use them. It turns out that using Xcode for this library management is quite cumbersome, so let’s see what we can do to automate this.
When you look at a typical Xcode project you see a bunch of code assets and a projectname.xcodeproj file. This file is actually a directory in disguise and contains all the project metadata. Most of this metadata is just settings for the Xcode user interface and is not directly related to our code.
The file that we are interested in is called project.pbxproj. This file contains the entire Xcode project structure. The project is described as an object graph, meaning that every entity in the project is a node that has an identifier, a type and zero or more references to other entities. The ideal representation for such a graph is a list of key-value pairs, and indeed the pbxproj file resembles this. Users of document oriented databases such as MongoDB should feel right at home.
Now when you open the pbxproj file in a text editor you see that the file has a syntax that is somewhat reminiscent of JSON. What you are looking at is in fact a plist file in the ancient (and even deprecated) NeXTSTEP ASCII format. Why Apple still uses this format for Xcode remains a mystery to me.
There are a couple of Ruby gems available to parse and manipulate pbxproj files. However, I find all of these gems too bloated for what they do. We can apply a bit of DRY by utilizing two existing tools.
On OS X there is a command line utility called plutil that, accoording to the manual page, is supposed to be used for syntax checking and plist format conversion. Of notable interest is that it can convert plist files to JSON, which we can parse using Ruby’s own JSON parser. Another interesting thing is that it can read all types of plist formats, including the NeXTSTEP format we are interested in.
This means that a simple pbxproj parser can be shrunk down to a single line of code:
def parse_pbxproj filename
JSON.parse(`plutil -convert json -o - "#{filename}"`)
end
This function returns a Hash object containing the entire object graph which you can then manipulate in whatever way you want. When the time comes to save the changes you can make use of these extra methods:
class String
alias to_plist to_json
end
class Array
def to_plist
items = map { |item| "#{item.to_plist}" }
"( #{items.join ","} )"
end
end
class Hash
def to_plist
items = map { |key, value| "#{key.to_plist} = #{value.to_plist};"
"{ #{items.join} }"
end
end
def save_pbxproj filename, hash
File.open(filename, "w") do |file|
file.write hash.to_plist
end
end
Now remember that the only types that you should use in a plist are strings, arrays and hashes. If you have anything else, like an integer, you will have to convert it to a string.
The methods above have been tested on about ten different projects using Xcode 4.1 and 4.2 beta and work perfectly. For best results reload your Xcode project after manipulating the pbxproj file with an external program, especially when developing for iOS.
Of course, this is just the beginning. I have used this parser myself to build a tool that can synchronize Xcode projects and GNU makefiles with good success. What will you do to accelerate your workflow?