Monday, August 8, 2011

iOS Continuous Integration - Part III

In the previous posts -- Part I & Part II -- we described how to setup your local GIT repository for working with submodules, then cloning the SampleLib repository and integrating with in Jenkins with automated testing and code coverage reports.

Now, we want to create a SimpleApp using the SampleLib and also applying Jenkins automation so to have a complete environment for your applications.

Again, feel free to get a copy of the project from my github account, or just follow along the instructions.

SampleApp

This assumes you already cloned the SampleApp repository under ~/Work/SampleApp (or whatever folder of your preference).
  • Launch Xcode4 and choose to Create a new Workspace (⌃⌘N)
  • Name is SampleApp and save it under ~/Work/SampleApp
  • Make sure you're viewing the "Project Navigator" (⌘1) and drag-n-drop the SampleLib project onto the Workspace
  • Now, create a New Project (⇧⌘N)
  • Choose Navigation-based Application (for this sample), call it SampleApp, and choose to "Include Unit Tests"
  • You should end up with something like the screenshot below:
  • You should be able to run SampleApp and see the empty TableView
  • Good time also to git commit everything so far

Using SampleLib

Let's adjust the build settings in order to use SampleLib. The advantage of doing this with Workspaces, rather than fat-libs, is that you let Xcode take care of choosing and building the right version (Debug vs Release and Device vs Simulator).
  • From the Project Navigator, go to SampleApp target -> Build Phases
  • From the left pane, just drag-n-drop SampleLib / Products / libSampleLib.a under "Link Binary with Libraries"
  • Under Build Settings set the following:
    • Linking -> Other Linker Flags -> Project level = -ObjC -all_load
    • Search Paths -> Always Search User Paths -> Project level = YES
    • Search Paths -> User Headers Search Paths -> Project level = $(BUILT_PRODUCTS_DIR)/**
  • We also need to make sure SampleLib is copying the headers
  • From the Project Navigator, go to SampleLib target -> Build Phases
  • Under "Copy Headers" phase, make sure NSDate+Humanize.h is copied to "Public"
  • Back to SampleApp make the following changes to RootViewController.m
//
//  RootViewController.m
//  SampleApp
//
//  Created by Rodrigo Lima on 8/8/11.
//  Copyright 2011 __MyCompanyName__. All rights reserved.
//

#import "RootViewController.h"
#import "NSDate+Humanize.h"

@implementation RootViewController

......................

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 5;
}

// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
    }

    switch (indexPath.row) {
        case 0:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:-(2*DAY_IN_SECONDS)];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 1:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:-DAY_IN_SECONDS];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 2:
        {
            NSDate* somedate = [NSDate date];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 3:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:DAY_IN_SECONDS];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;
        case 4:
        {
            NSDate* somedate = [NSDate dateWithTimeIntervalSinceNow:(2*DAY_IN_SECONDS)];
            cell.textLabel.text = [somedate humanizedDescription];
        }
            break;

        default:
            break;
    }

    // Configure the cell.
    return cell;
}

......................

@end
  • Build and run, you should see the following:

Saving Changes

As we had to make a small change to SampleLib in order to copy the header files to a public location, now we're in a situation where the git submodule also has changes that need to be committed. The git submodule tutorial describes this situation under the Gotchas subsection. It says: "Always publish the submodule change before publishing the change to the superproject that references it. If you forget to publish the submodule change, others won't be able to clone the repository" Thus, let's do the following:
rolima@mac ~/Work/src/SampleApp (master)$ git st
# On branch master
#
#  modified:   Library/SampleLib (modified content)
#  modified:   SampleApp/SampleApp.xcodeproj/project.pbxproj
#  modified:   SampleApp/SampleApp/RootViewController.m
#
no changes added to commit (use "git add" and/or "git commit -a")

rolima@mac ~/Work/src/SampleApp (master)$ cd Library/SampleLib/
rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ git st
# On branch master
#
#  modified:   SampleLib.xcodeproj/project.pbxproj
#
no changes added to commit (use "git add" and/or "git commit -a")

rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ git commit -a -m "headers should be public"
[master 114b3ae] headers should be public
 1 files changed, 1 insertions(+), 1 deletions(-)

rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ git push
Counting objects: 7, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 429 bytes, done.
Total 4 (delta 2), reused 0 (delta 0)
To git@github.com:rodrigo-lima/SampleLib.git
   6e62f27..114b3ae  master -> master

rolima@mac ~/Work/src/SampleApp/Library/SampleLib (master)$ cd ../..
rolima@mac ~/Work/src/SampleApp (master)$ git add .
rolima@mac ~/Work/src/SampleApp (master)$ git st
# On branch master
# Changes to be committed:
#
#  modified:   Library/SampleLib
#  modified:   SampleApp/SampleApp.xcodeproj/project.pbxproj
#  modified:   SampleApp/SampleApp/RootViewController.m
#

rolima@mac ~/Work/src/SampleApp (master)$ git commit -a -m "using SampleLib"
[master f51e25d] using SampleLib
 3 files changed, 54 insertions(+), 2 deletions(-)

rolima@mac ~/Work/src/SampleApp (master)$ git push
Counting objects: 15, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (8/8), 1.18 KiB, done.
Total 8 (delta 4), reused 0 (delta 0)
Unpacking objects: 100% (8/8), done.
To /Users/rolima/Work/CODE/blog/GIT-REPO/public/SampleApp.git
   285951e..f51e25d  master -> master
rolima@mac ~/Work/src/SampleApp (master)$ 

Automation, Automation, Automation

As we're building a Xcode workspace in Jenkins, we need a quick trick so that Jenkins is able to run successfully. I looked for ways of automating this, but still could not find it. If you know, please leave a comment :-)

The problem is that we're not saving some user-specific metadata to GIT. Thus, when Jenkins checks out the project the workspace is not 'fully prepared'. You need to open it in Xcode in order to be able to run it successfully.

So, let's start by going to your Jenkins' Dashboard and choosing to create a New Job
  • Job Name = SampleApp
  • To simplify, Copy existing job = SampleLib
  • Source Code Management = Git
  • Repository = /Users/YOURUSERNAME/Work/GIT-REPO/public/SampleApp.git or your GitHub repository URL
  • Build Triggers = Build after other projects are built = SampleLib
  • Build Triggers = Poll SCM (or any other) = * * * * * (to poll every minute) -- feel free to adjust to your likings
  • Build - add an Execute Shell step:
    xcodebuild -workspace SampleApp.xcworkspace -scheme SampleApp -sdk iphonesimulator4.3
  • Post-build Actions - do not check anything right now
Now, run the build once so Jenkins checks out the project. Don't worry if it fails, that's what we're fixing now...

From a terminal window:
rolima@mac ~ $ cd /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace/
rolima@mac /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace ((no branch))$ xcodebuild -workspace SampleApp.xcworkspace -list
2011-08-08 13:51:54.828 xcodebuild[17224:a0b] WARNING: Timed out waiting for /"runContextManager.runContexts" (10.001445 seconds elapsed)
There are no schemes in workspace "SampleApp".
See the problem? So, just fire Xcode and open the Workspace (⌘O) from /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace/SampleApp.xcworkspace. That's it, now close the Workspace and run the command again from Terminal:
rolima@mac /Users/Shared/Jenkins/Home/jobs/SampleApp/workspace ((no branch))$ xcodebuild -workspace SampleApp.xcworkspace -listInformation about workspace "SampleApp":
    Schemes:
        SampleLib
        SampleApp
  • Now we're good to go... Just go back to Jenkins and run another build
  • All should go well and you now have a Jenkins jobs for the SampleApp.

What's left to do?

Now that you have the environment ready, you just need to implement the Test cases for SampleAppTests and repeat the configuration steps on both Xcode and Jenkins in order to be able to run unit tests reports and code coverage.

If you don't remember how to do it, just take a look at Part II.

Have fun!

2 comments:

  1. Nice post, saved a lot of my time with that
    There are no schemes in workspace "SampleApp"
    Problem.

    ReplyDelete
  2. If you do not want to open the Workspace, but are receiving the "There are no schemes in workspace x" message, go to Schemes and click the Shared checkbox. Xcodebulid should now be able to view all schemes without needing to open the Workspace in Xcode.

    ReplyDelete