Thursday, August 4, 2011

iOS Continuous Integration

Background

Recently we've been re-organizing our code looking for common/re-usable components and extracting them as libraries. The idea is to organize the code in such a way that we can have people dedicated on the core functionality (REST/JSON parsing), object layer, and UI components.

This separation also helps us with testing and code coverage, as the person responsible for a library is also responsible for its unit testing. I've been researching ways of organizing code in XCode and also Continuous Integration best practices.

There's a bunch of information out there and here are some very helpful links:
Libraries
Continuous Integration & Testing

Based on the research, we ended up with something like:

  • split the code in libraries -- core, model, UI, etc.
  • use XCode4's Workspaces, rather than compile fat-libraries. Let XCode4 take care of compiling and linking the right version of library for us.
  • distribute and use the libraries in other projects as GIT submodules.
  • automate CI with Jenkins + Unit Test + Code Coverage.


Sample Project

I'll break the post in three parts. Part I will present the sample project and setup the GIT repositories for it. Part II will talk about CI + Unit Testing + Code Coverage for the Sample Lib, and finally Part III talks about the SampleApp and how everything comes together so you can have a great development environment for your iOS projects.

The Sample Project is really simple and consists of:
  • SampleLib - simple library with a Category on NSDate to print humanized dates
  • SampleApp - displays a TableView with a bunch of dates
Note: The main goal is not actually the project, but the steps to configure it to achieve automated CI.

Part I - Setting up Repositories

This is to configure the *GIT server* repository. It should be done only *once* when setting up a new GIT server to host the project files. The way I did for this post was to setup and work on local repository, and later push it to github. Steps:
  • choose a location for the server repository, i.e. ~/Work/GIT-REPO
  • run the script below to setup the following repositories:
    • *SampleLib* - hosts the Library sources
    • If you have more libraries, you can easily modify the script below to setup it up all at once.
  • this script creates folders private and public
    • private - contains initial setup files - readme & .gitignore
    • public - the .git file used for cloning this repository
Note: The script assumes both files below are present under ~/Work/GIT-REPO:
  • gitignore_template - template for .gitignore
  • setupLibs.sh - shell script to create the repositories
#### `.gitignore`
# xcode noise
build/*
dist/*
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
*.xcuserdatad/
profile

#automated testing & code coverage
test-reports/*
coverage/*

# osx noise
.LSOverride
*.swp
.DS_Store
#!/bin/sh

ROOT_REPO=~/Work/GIT-REPO

mkdir -p ${ROOT_REPO}/public
mkdir -p ${ROOT_REPO}/private
cd ${ROOT_REPO}/private

for mod in SampleLib; do
 echo "Initializing GIT structure for $mod..."   
 mkdir $mod
 cd $mod
 git init
 cp ${ROOT_REPO}/../gitignore_template ./.gitignore
 echo "$mod Readme" > Readme.md
 git add .
 git commit -m "Initial commit, setting up $mod"
 git clone --bare . ${ROOT_REPO}/public/$mod.git
 git remote add origin ${ROOT_REPO}/public/$mod.git
 git config branch.master.remote origin
 git config branch.master.merge refs/heads/master
 cd ..
done

echo "DONE!"

Repository Setup -- SampleApp

  • Client application will reference SampleLib libraries by using Xcode4 Workspaces
  • SampleLib will be added as submodules of SampleApp. This is useful also if you want to add other 3rd party libraries, like three20
  • The script below automates this process when setting up your GIT repository
#!/bin/sh

ROOT_REPO=~/Work/GIT-REPO

cd ${ROOT_REPO}/private

echo "Initializing GIT structure for SampleApp..."
for mod in SampleApp; do
 mkdir $mod
 cd $mod
 git init
 cp ${ROOT_REPO}/../gitignore_template ./.gitignore
 echo "$mod Readme" > Readme.md
 mkdir Library
 echo "Libraries folder" > Library/Readme.md
 git add .
 git commit -m "Initial commit, setting up $mod"
 git clone --bare . ${ROOT_REPO}/public/$mod.git
 git remote add origin ${ROOT_REPO}/public/$mod.git
 git config branch.master.remote origin
 git config branch.master.merge refs/heads/master
 cd ..
done

echo "Setting up GIT Submodules...."
cd SampleApp
#SampleLib -- assuming local
git submodule add ${ROOT_REPO}/public/SampleLib.git Library/SampleLib

#SampleLib -- assuming github
#git submodule add git://github.com/rodrigo-lima/SampleLib.git Library/SampleLib

#External libs -- for example three20
#git submodule add git://github.com/facebook/three20.git Library/three20

#initial commit
git add .
git commit -m "Adding submodules."
git push

echo "DONE!"

Using the Development Repository -- SampleLib

The beauty of having separate repositories for your libraries is that you can have individuals responsible for the whole coding process for each library, including writing the test cases. Another advantage with the proposed setup, is that Jenkins will be building, testing and generating code coverage reports automatically for each commit. A person who wishes to work on the SampleLib just needs to:
  • go to your development folder and clone the SampleLib repo
  • open SampleLib.xcodeproj with XCode
  • code away :)
rolima:~ $ cd ~/Work/
 rolima:~/Work $ git clone ~/Work/GIT-REPO/public/SampleLib.git
 rolima:~/Work $ cd SampleLib
 rolima:~/Work/SampleLib $ 

Using the Development Repository -- SampleApp

For the person responsible for working on the actual App, he just needs the following in order to clone it from GIT and start working:
  • go to your development folder and clone the ClientApp repo
  • must also init & update submodules
  • open SampleApp.xcworkspace with XCode - NOTE - it's the Workspace
rolima:~ $ cd ~/Work/
rolima:~/Work $ git clone ~/Work/GIT-REPO/public/SampleApp.git
rolima:~/Work $ cd SampleApp
rolima:~/Work/SampleApp (master)$ cd SampleApp
rolima:~/Work/SampleApp (master)$ git submodule init
....

rolima:~/Work/SampleApp (master)$ git submodule update
.....

rolima:~/Work/SampleApp (master)$
Note that once you run git submodule init/update, the submodules will be on a detached branch, so you need to check it out to some branch before making any work.
rolima:~/Work/SampleApp (master)$ cd Library/SampleLib
rolima:~/Work/SampleApp/Library/SampleLib ((no branch))$ git checkout master
rolima:~/Work/SampleApp/Library/SampleLib (master)$ cd ../../
rolima:~/Work/SampleApp (master)$ 
We now have the environment ready for Part II .
In future, if you want to send this repository to your github account, you just need to follow the instructions from github -- Manual clone and push. First, create a new repo on the target account. Next, create a mirror that includes all branches and tags. To do this you need to do a bare-clone followed by a mirror-push:
rolima:~/Work/tmp $ git clone --bare ~/Work/GIT-REPO/public/SampleLib.git
rolima:~/Work/tmp $ cd SampleLib
rolima:~/Work/tmp/SampleLib $ git push --mirror git@github.com:mycompany/SampleLib.git
rolima:~/Work/tmp/SampleLib $ cd ..
rolima:~/Work/tmp $ rm -rf SampleLib

1 comment:

  1. I think it should also be interesting to you and your readers – I'm developing hosted CI service for iOS projects: http://hosted-ci.com

    I think it should be simpler way to manage things than running Jenkins on own hardware.

    ReplyDelete