Friday, August 12, 2011

iOS Continuous Integration - Part IV

My previous posts described a way for you to have automated testing and code coverage for your iOS projects using Jenkins.
Now that we have that in place, we can also rely on Jenkins to generate Ad Hoc & Distribution builds. Even more interesting, we can have Jenkins deploy these new binaries and notify your alpha/beta-testers there is a new version for them to play with.

References

There is actually a bunch of information out there on how to do this.

General Information:
Distribute via Xcode's Archive Gotchas - when working with static libraries (i.e. SampleApp) TestFlightApp Manually generating .ipa

Preparing SampleApp for IPA

Our SampleApp project, although very simple, tries to mimic a real project, where you have 3rd party and/or your own libraries all inside a Xcode4's workspace.

Because of that, if you just go ahead and try to "Product -> Archive" it'll most likely fail. That's why there a few links under the "Gotchas" section above :)

Let's prepare SampleApp so that is able to generate an IPA from within Xcode. Even if you don't actually build the IPA from Xcode, is good to have it all prepared just in case you want to do it.

SampleLib Project
  • Deployment = Skip Install = Yes - set in project level
  • Packaging = Public Headers Folder Path = Coverage & Debug = ../include/$(PROJECT_NAME)
  • Packaging = Public Headers Folder Path = Release = ../../include/$(PROJECT_NAME)
SampleApp Project
  • Search Paths = User Header Search Paths = $(BUILT_PRODUCTS_DIR)/../include $(BUILT_PRODUCTS_DIR)/../../include $(BUILT_PRODUCTS_DIR)/** - set in project level
  • RootViewController.m, change the import statment to #import <SampleLib/NSDate+Humanize.h>
  • would also be nice to add an icon for your app...
With these settings, you should be able to Run (⌘R - which builds Debug), Profile (⌘I - which builds Release), and Archive.
Note: if Archive is grayed out, that's because you don't have SampleApp -> iOS Device scheme selected.

Quickest Way

If you don't want to use Jenkins and just want to get you App out there for people to give it a try, I'd say the best is to use TestFlightApp.com.

Automated Way - Basic

If your project is not contained on a Xcode4 Workspace, then my suggestion is just to follow the instructions from Shine Tech's blog. It relies on the XCode plugin for Jenkins and describes how to automatically deploy your IPA to TestFlightApp.com.

Automated Way - Advanced

As our SampleApp is currently contained on XCode4 Workspace, we cannot rely on XCode plugin for Jenkins as it does not currently supports workspaces.

That's not a problem, we can manually build the IPA by running a simple script (thanks to Mike Nachbaur's post). We'll also use Jenkins to copy the build IPA to your Dropbox account and send a message to your testers, so then can play with the new version (and log a bunch of new bugs for you :).

We'll be also updating the Build version (CFBundleVersion) using the current date & build version. It's good for tracking purposes, but feel free to include it or not in your project.

SampleApp
  • add the following configuration file under a newly created scripts folder:
    rolima:~ $ cd ~/Work/SampleApp
    rolima:~/Work/SampleApp (master)$ mkdir scripts
    rolima:~/Work/SampleApp (master)$ cd scripts
    rolima:~/Work/SampleApp (master)$ vi build.config
    
    # For AdHoc, I'd rather send a DEBUG build.
    CONFIGURATIONS="Debug Release"
    
    # AdHoc Debug settings
    Debug_ProvisionFile=AdHoc.mobileprovision
    Debug_Codesign="iPhone Developer"
    
    # Release settings
    Release_ProvisionFile=AppStore.mobileprovision
    Release_Codesign="iPhone Distribution"
    
    # OTA settings -- used to build the link
    OTAURL="http://dl.dropbox.com/u/YOUR_USER_ID/MyApps"
    OTASmallIcon=SampleApp/icon.png
    OTALargeIcon=SampleApp/icon-512.png
    OTADeployPath=~/Dropbox/Public/MyApps
    OTATitle=SampleApp
    OTASubtitle=SampleApp for blog post
        
  • Also, let's add a Run Script build phase to SampleApp, so that Jenkins is able to find the SampleApp.app
    echo "Make sure Output folder exists..."
    mkdir -p $SRCROOT/../output
    
    echo "Save path to SampleApp.app..."
    echo $CODESIGNING_FOLDER_PATH > $SRCROOT/../output/$PROJECT_NAME.app.filepath.txt      
        
Jenkins
  • Let's start by creating a new job on Jenkins, so that we can schedule when it will build, and not mess with testing/code coverage automation jobs already there.
  • Job Name = SampleApp-AdHoc - better if it does not have spaces, as it will be part of the IPA file name
  • Build a free-style software project
  • Source Code Management = Git
  • Repository = /Users/YOURUSERNAME/Work/GIT-REPO/public/SampleApp.git or your GitHub repository URL
  • Build Triggers = Build periodically = @weekly - for example, if you want a new AdHoc every week. Feel free to play with these settings
  • Build - add an Execute Shell magic step that: bumps CFBundleVersion, compiles the app, signs it, generates the IPA, creates the plist for the app, and copy all that is needed to a Dropbox public folder. You should modify this accordingly to whatever your needs/requirements for deployment.
    #!/bin/sh
    function failed()
    {
      echo "Failed $*: $@" >&2
      exit 1
    }
    
    # properties
    export OUTPUT=$WORKSPACE/output
    rm -rf $OUTPUT
    mkdir -p $OUTPUT
    PROFILE_HOME=~/Library/MobileDevice/Provisioning\ Profiles/
    
    # read build configuration 
    . "$WORKSPACE/scripts/build.config"
    
    # bump version
    BUNDLE_VERSION=`date +'%y%m%d'`.$BUILD_NUMBER
    cd SampleApp
    agvtool new-version -all $BUNDLE_VERSION
    cd ..
    
    # process builds 
    for config in $CONFIGURATIONS; do
      echo "\n==================================================================\n"
      echo "COMPILE $JOB_NAME-$BUILD_NUMBER-$config ..."  
    
      provfile=$(eval echo \$`echo ${config}_ProvisionFile`)
      codesign=$(eval echo \$`echo ${config}_Codesign`)
      cert="$PROFILE_HOME/$provfile"
      fileprefix="$JOB_NAME-$BUILD_NUMBER-$config"
      ipaname="$fileprefix.ipa"
      otaname="$fileprefix.plist"
    
      #invoke build on workspace
      xcodebuild -workspace SampleApp.xcworkspace -scheme SampleApp -configuration $config -sdk iphoneos4.3 build || failed build;
    
      #generate the IPA
      app_path=$(cat output/SampleApp.app.filepath.txt)
      xcrun -verbose -sdk iphoneos PackageApplication \
          "$app_path" -o "$OUTPUT/$ipaname" \
          --sign "$codesign" --embed "$cert"
    
      echo "\n==================================================================\n"
      echo "DEPLOY $OTADeployPath/$fileprefix...."
    
      #make sure folder exists
      mkdir -p $OTADeployPath/$fileprefix
      #copy icons
      cp $WORKSPACE/$OTASmallIcon $OTADeployPath/$fileprefix/Icon-57.png
      cp $WORKSPACE/$OTALargeIcon $OTADeployPath/$fileprefix/Icon-512.png
      #copy IPA
      cp $OUTPUT/$ipaname $OTADeployPath/$fileprefix
    
      #read properties
      info_plist=$(ls SampleApp/SampleApp/*Info.plist | sed -e 's/\.plist//')
      bundle_version=$(defaults read $WORKSPACE/$info_plist CFBundleVersion)
    
      #generate PLIST
      cat << EOF > $OTADeployPath/$fileprefix/$otaname
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
       <key>items</key>
       <array>
           <dict>
               <key>assets</key>
               <array>
                   <dict>
                       <key>kind</key>
                       <string>software-package</string>
                       <key>url</key>
                       <string>$OTAURL/$fileprefix/$ipaname</string>
                   </dict>
                   <dict>
                       <key>kind</key>
                       <string>display-image</string>
                       <key>needs-shine</key>
                       <true/>
                       <key>url</key>
                       <string>$OTAURL/$fileprefix/Icon-57.png</string>
                   </dict>
                   <dict>
                       <key>kind</key>
                       <string>full-size-image</string>
                       <key>needs-shine</key>
                       <true/>
                       <key>url</key>
                       <string>$OTAURL/$fileprefix/Icon-512.png</string>
                   </dict>
               </array>
               <key>metadata</key>
               <dict>
                   <key>bundle-identifier</key>
                   <string>com.rolima.SampleApp</string>
                   <key>bundle-version</key>
                   <string>$bundle_version</string>
                   <key>kind</key>
                   <string>software</string>
                   <key>title</key>
                   <string>$OTATitle</string>
                   <key>subtitle</key>
                   <string>$OTASubtitle</string>
               </dict>
           </dict>
       </array>
    </dict>
    </plist>
    EOF
    
    done
        
  • We now just need to email people with the link to the new IPA, so they can proceed with their testing.
  • Just check Editable Email Notification and add something like this for the email message text:
    <h3>$PROJECT_NAME - Build # $BUILD_NUMBER - $BUILD_STATUS</h3>
    <p>
    Check console output at $BUILD_URL to view the results.
    </p>
    <p>
    To install this version on your iPhone, just click 
    <a href="itms-services://?action=download-manifest&
    url=http://dl.dropbox.com/u/YOUR_USER_ID/MyApps/$PROJECT_NAME-$BUILD_NUMBER-Debug/
    $PROJECT_NAME-$BUILD_NUMBER-Debug.plist">here</a>.
    
    If you have any problems, please contact 
    <a href="mailto:administrator@yoursite.com">me</a>
    </p>
    <hr/>
    <b>Changes</b>
    <code>
    ${CHANGES}
    </code>
        
  • Note that I used the Debug version of the app, and that is currently using the AdHoc.mobileprovision (see build.config). That requires that the UUID of the tester's device to be registered so he/she is able to install and run the app.
  • Under the Advanced section for Editable Email Notification, you need to add a trigger for Success builds otherwise you'll not receive emails. Note that you can also add different recipients for each build case -- failure, success, etc -- so you can email your testers only when the build is successful.
  • You should be able to save the Jenkins job, run it and start receiving the email notifications.
Have fun!

1 comment:

  1. Either way, it may be helpful for you to become versed on some of the most common iOS app development terms and protocols that can help your unique organization run smoother and become more profitable. Read below as we discuss some of these and offer advice for your project, and good luck as you embark upon a journey to leverage the technological flexibility of applications to fit your needs.

    ios jobs

    ReplyDelete