Building a static library with Jenkins
One of my pet peeves is Open Source iOS libraries distributed as just a collection of Objective-C classes, rather than being bundled as a static library. I know a lot of people prefer it that way, but from a maintainability standpoint it really doesn’t make much sense to me. So when I’m faced with another library I want to use that doesn’t have a static library readily available for it, I typically wrap it up in my own Xcode project, check it in to Github, and configure my Jenkins continuous integration build server to compile it for me.
I thought I’d walk you through the steps I go through to make this happen, so you can use this technique too.
Why I prefer static libraries
Before I go too far on this topic I wanted to explain my reasoning behind preferring static libraries, as opposed to loosely-distributed sets of classes.
Consider a large project where I’m utilizing a few libraries built by third-parties to speed up my development; a recent example of this on one of my projects is using the AFNetworking library from Gowalla. Now I could simply sync their source tree from Github, drag-n-drop their classes into my project, and I could be done with it. But what happens when one of their classes is upgraded? I have to re-sync the code, resolve any differences, and drag it back into my project. I could of course make their project a git sub-module of my own, but then whenever I perform an update to my git project I may not know which changes their source has undergone, meaning I have no control whether or not my project will pass or fail. And this assumes I’m using Git for my project; at work we use Perforce which has terrible Git integration.
Assuming I have an up-to-date set of classes from a third-party integrated into my project, what happens in the event I change some C pre-processor definitions that break those classes? Since every time I compile my code I have to also compile the third-party code, I don’t know if a compilation bug in the third-party classes are a problem caused by my build settings, or is a problem in their code. If I were to consume a static library I could re-use that binary safe in the knowledge that no random changes will take place in it. If I wanted to bring in any updates from a third-party I can choose when to integrate those updates in since I can choose when to copy over the new library.
And finally, the reason why I had to create a static library for AFNetworking in the first place, is in the transition from manual reference counting to “Automatic Reference Counting” (also known as ARC). ARC requires actual code-level changes since such method calls as “release”, “autorelease”, “dealloc” etc are now a compilation error. This means you can’t mix-and-match ARC and non-ARC classes in the same project. Non-ARC code compiled in its own separate static library though can still play nice within a different ARC project.
How to create a static library
First you should begin by either forking the target library on Github, or by acquiring the source code in some other way. If you fork from Github it will make it easier to merge changes down later as the project is updated in the future, or provides for the ability to merge your static library up into the main project.
Regardless, once you get the code you begin by creating a new static library project in Xcode. It’s simple enough; just make sure you select the “Cocoa Touch” static library project and you should be good to go. Once this opens up it will give you some boilerplate classes and settings; feel free to delete those. You might want to create the new project somewhere within the source tree for the library in a way that won’t annoy other developers. For example, you might not want to move the original classes around too much since it might make it difficult for other developers to merge your change into their own branches.
Once you have your empty library project you can go ahead and add all the classes you’ll be wanting to include in the library, either by using the “Add files to…” menu item, or by drag-and-dropping them from Finder into an appropriate folder in the navigation pane of your project.
At this point the code might compile, but you need to make some changes to the project settings in order to make it possible to compile, and to make it easy for you to get at the built results of the project. Click on the root of your project to open the project settings, and make the following changes:
- Installation Directory (INSTALL_PATH): Set this to “/”. This indicates where the products of the build (namely the static libAFNetworking.a file) will be installed, relative to the target installation root.
- Public Headers Folder Path (PUBLIC_HEADERS_FOLDER_PATH): This controls where the public headers for your library will be installed, relative to the installation root. Set this to “Headers”.
- Header Search Paths (HEADER_SEARCH_PATHS): This isn’t strictly necessary for all projects, but in this case we’re going to compile against the JSONKit library, but are not going to be including it in the resulting static library. So we need to tell Xcode where to find the header so it can properly compile the classes that might potentially depend on it.
- **Skip Install (SKIP_INSTALL):**Set this to “No” or, in the example in the above screenshot, set delete it so the Xcode default of “No” will take effect. This is important because without this we won’t be able to install the results of this build into a directory where we can get at it.
- Precompile Prefix Header & Prefix Header (Optional): In the example I’m showing here I didn’t actually want to add more unnecessary files to the project than needed, and since the prefix header only adds a global import of “Foundation.h”, I removed those from the Xcode project.
You can see in the screenshot that I’ve moved all of the AFNetworking headers into the Public section, and left JSONKit in the Project section. This is because if a user of this library wants to use JSONKit they’ll presumably get it on their own, so any headers we include here might result in a conflict. So it’s best to just include the headers for the code that is actually present in your project.
If you’re creating an SDK or a reusable API for others to use and there are some parts of your code that you would rather remain hidden, simply don’t move those headers into the “Public” section.
Building your library in Jenkins
At this point your code should compile cleanly when you click the “Build” button. But more than that we want to be able to simplify the compilation of this library so it can easily be packaged up and shipped off. I like to either use shell scripts or “Ant” to automate my build process since it allows me to predictably run my builds deterministically with little room for human error.
There are several things we want the build script to accomplish:
- Create a directory where the results of the build will reside;
- Compile versions of the static library for both the device and the simulator;
- Create a “FAT” library containing both the device and simulator results for easy debugging;
- Create a ZIP file containing the static library and its headers.
#!/bin/sh
set -ex
INSTALL_PATH=$WORKSPACE/artifacts
[ -z $INSTALL_PATH ] || INSTALL_PATH=$PWD/artifacts
rm -rf $INSTALL_PATH
mkdir -p $INSTALL_PATH
PROJ=AFNetworking/AFNetworking.xcodeproj
xcodebuild -project $PROJ -sdk iphoneos INSTALL_ROOT=$INSTALL_PATH/device install
xcodebuild -project $PROJ -sdk iphonesimulator INSTALL_ROOT=$INSTALL_PATH/simulator install
lipo -create -output $INSTALL_PATH/libAFNetworking.a $INSTALL_PATH/device/libAFNetworking.a $INSTALL_PATH/simulator/libAFNetworking.a
mv $INSTALL_PATH/device/Headers $INSTALL_PATH
rm -rf $INSTALL_PATH/device $INSTALL_PATH/simulator
(cd $INSTALL_PATH; zip -r ../AFNetworking.zip libAFNetworking.a Headers)
If you understand Bourne Shell that code should be pretty straight-forward. You simply set up an install path relative to the Jenkins workspace (or relative to your current directory if you’re running the script by hand), run two separate builds using “xcodebuild”, use “lipo” to merge the resulting static libraries, and we create a ZIP file of the resulting files.
I’ll go through each part of the “xcodebuild” command to point out why I’m doing things this way.
- “xcodebuild -project $PROJ” – Indicate the project file that you want to build. I’m using a variable defined higher up in the script to make it easy to change.
- “-sdk iphoneos” or “-sdk iphonesimulator” – Manually specifying the SDK to use so we can compile both the device and simulator versions.
- “INSTALL_ROOT=…” – This passes a variable into the build process, allowing us to customize the path the resulting artifacts will be installed to. By default, as you can see from the above screenshots, that the path “/tmp/AFNetworking.dst” is defined. This command-line argument lets us override that setting to configure a path of our choosing.
- “install” – Perhaps the most important part, we’re saying we want to install the files, rather than just simply building them. Xcode normally creates its build artifacts in a folder in ~/Library/Developer/Xcode/DerivedData, keyed off some awfully long UUID, and there’s no easy and deterministic way of identifying the path Xcode will choose when building its artifacts. When using “install” the appropriate files are copied from that interim build directory and into the directory you selected above.
Getting Jenkins to rebuild when code is checked in
The final and most useful step is to get your code rebuilding automatically when you check new code into Github. This technique will work for other version control systems, but Github is certainly the easiest to control.
Make sure your build script is checked into your source tree, and configure a new job in Jenkins for your static library project. I won’t walk you through the entire process, but it’s simple enough to create a “Freestyle” project, set up your Github credentials for checking code out when a build is triggered. From there, add a new “Execute Shell” build step, and add the following to the command:
"$WORKSPACE/build.sh"
Make sure you include the quotes, because your $WORKSPACE path that Jenkins assigns you may potentially contain whitespaces that would break that command. From there you should be able to trigger a new build manually and have it compile your project for you. Give it a try and make sure it works.
Next you’ll want to do something with the resulting build artifacts. What I like to do is create a single directory under which all my compiled resources will be collected and archived by Jenkins, but since the above build script created a single convenient ZIP file we can just archive that. Check the “Archive the artifacts” box, and type “AFNetworking.zip” into the field.
At this point we’re finally able to integrate our build into Github, but it’s a lot simpler than you might think. Jenkins has an amazing REST API that lets you interact with it programmatically. If you want to kick off a new build, all you need to do is perform an HTTP GET against the “build” URL for your job. If your Jenkins server is located at http://jenkins.mycompany.com/
and your job is named “MyLibrary”, you only need to issue a GET to http://jenkins.mycompany.com/job/MyLibrary/build
.
As you can see from the screenshot to the right, it’s fairly simple and is easy to test. Next time you perform a “git push” back up to Github, your Jenkins server should automatically trigger a new build.
If you want to see everything I’ve talked about here, go to my Github fork of AFNetworking created from the process I describe above.