Custom Build Phases and Scripts

I’m a fan of unit testing. It has saved me from much embarrassment during my career. But I find tests tedious to write and often worry that they pass not because the results are correct but because the tests are wrong. Which is long way of saying that I’m also a huge fan of compilers. A critical portion of my testing is writing code that forces the compiler to verify my code. And if I can get that with minimal work, so much the better.

So one bug, that I manage to repeat just about every project, is errors with the constants that reference resources. I get the names of PNG files, tags and identifiers wrong all the time. Or I change the name in the resource, even something simple like capitalization. Fortunately with enough testing these errors come to light. But I’ve also solved this problem before. And in a way that uses the compiler to test for some form of correctness. The simple solution is to create a process that preprocesses the resources and creates a source file with all the constants.

But how to drive the preprocess? Xcode custom build phases and custom build scripts are nearly ideal for this type of work and they’re easy to set up. Just select the project root in the navigator, then the target and finally either the build phase or build rules.

This is the Build Phases Panel. Build Phase scripts run once for every compile. I use these for output that is affected by many files, such as a list of all PNGs. In the bottom right corner is the Add Build Phase picker. Select “Add Run Script” to add a new Build Phase script. The ordering of phases can be easily changed by dragging the Run Script title bar. Make sure that this comes before the compile sources because the compile is going to use the results of this output.

I generally find it useful to have the majority of the script in a separate file. That way I can easily reuse the functionality. Its also helpful while debugging the script to be able to run it directly from the command line.

Those actions that are more specific to the project I tend to not place in the script file. The ‘cd’ and ‘cp’ in the illustration above are examples of this. Even as I write this I’m not fully convinced that this is the best solution. The nice thing is that if I change the script and it doesn’t work the compiler will tell me something is wrong. To me thats testing early and testing often.

This is the Build Rules Panel. The Add Build Rule is a simple button. Build Rules are run once for each file, and the Process menu can be used to select which type of file it is run with. You could replicate the functionality of a Build Rule with a Build Phase that walks the project hierarchy. The advantage of a Build Rule is that it is sensitive to changes to the input file and will only process files that have an actual change. It also ensures that the input files are actually in the project.

The Build Phase script that I’ve included in this project creates a list of defines for the PNG files. Its written in Ruby. I’m not a Ruby programmer so it may be a bit rough.

#! /usr/bin/env ruby

dest = File.open(ARGV[0],File::CREAT|File::WRONLY,0777)

Dir.foreach(".") { |file|
    expression =  /.png$/
    if expression.match file #filename ends with .png
        name = File.basename(file,".png")
        dest.puts "#define k_png_"+ name + " @\"" + name + "\""
    end
}

dest.close

and its output is

#define k_png_glyphicons_002_dog @"glyphicons_002_dog"
#define k_png_glyphicons_031_bus @"glyphicons_031_bus"
#define k_png_glyphicons_042_group @"glyphicons_042_group"
#define k_png_glyphicons_070_umbrella @"glyphicons_070_umbrella"
#define k_png_glyphicons_132_inbox_minus @"glyphicons_132_inbox_minus"
#define k_png_glyphicons_143_database_ban @"glyphicons_143_database_ban"
#define k_png_glyphicons_158_playlist @"glyphicons_158_playlist"
#define k_png_glyphicons_193_circle_ok @"glyphicons_193_circle_ok"
#define k_png_glyphicons_264_fishes @"glyphicons_264_fishes"
#define k_png_glyphicons_304_vimeo @"glyphicons_304_vimeo"

Your could perform all sorts of remapping of the of the characters, such as all caps or camel case and remove the underscores, but I prefer that generated items be as easily identified with their source as possible and so do a minimal amount of processing.

The Build Script script uses an xml parser to extract the attributes of various elements and again uses a very simple define for the mapping.

#! /usr/bin/env ruby

require "rexml/document"
dest = File.open(ARGV[1],File::CREAT|File::WRONLY,0777)
src = REXML::Document.new File.open(ARGV[0])

src.elements.each("//@storyboardIdentifier").each do
    |element|
    if element.value != ""
        dest.puts "#define k_storyboard"+ element.value + " @\"" + element.value + "\""
    end
end

src.elements.each("//@reuseIdentifier").each do
    |element|
    if element.value != ""
        dest.puts "#define k_reuseIdentifier"+ element.value + " @\"" + element.value + "\""
    end
end

src.elements.each("//@tag").each do
    |element|
    if element.value != ""
        dest.puts "#define k_tag"+ element.value + " @\"" + element.value + "\""
    end
end

dest.close

and its output is

#define k_storyboardMasterObject @"MasterObject"
#define k_storyboardTableController2 @"TableController2"
#define k_reuseIdentifierTitle_1 @"Title_1"
#define k_reuseIdentifierCell_Type_2 @"Cell_Type_2"
#define k_tag12345 @"12345"

The final step in both of these is to copy the files produced back to a location that can be referenced from your normal source code. By convention I create a directory to hold all the generated files. This makes it really easy to identify them so you don’t do things like edit by hand.

This technique doesn’t solve all errors. You can still reference the wrong file, tag or identifier, rather that a nonexistent one. But it does prevent the most common error with little effort. And any help, however minor, adds to my satisfaction with the result.

Theres a demo project here. I’m still refining the scripts and maybe next time I’ll include type checked code rather than just pound defines. Wouldn’t it be cool to be able to write [[CodeGenerator shared] loadPNG_foo] and know that it will work.

This entry was posted in iDevBlogADay. Bookmark the permalink.

Comments are closed.