Tuesday, November 27, 2012

Building PhoneGap / Apache Cordova iOS Apps with Jenkins

While there are several posts related to Jenkins and PhoneGap, I couldn't find a complete, up-to-date, 'step by step' guides that show how to setup continuous integration builds for your team.

I wanted to share my experience with both tools, but first let's start with some assumptions and requirements:
  1. As of today the most updated development environment version is XCode 4.5 w/ iOS 6.0. It is highly recommended to update to the latest and greatest version so things work properly.
  2. I used the latest Jenkins which is downloadable from its download site
  3. After installing Jenkins, I used this cool xcode-plugin that really helps to get things done. You can install it by going to Manage Jenkins | Manage Plugins | Available | Search for xcode and install it.
  4. As an optional step you should also install the Amazon S3 plugin to push the "Enterprise Site" artifacts (the IPA and PLIST files) so it can be available to others in your team.
  5. You must have Apple's iOS Developer Program account. Make sure you create a Certificate and Distribution (Adhoc) provisioning profile using the portal. Start by creating a certificate for Distribution (remember to do this through the "Distribution" tab) and continue with adding the devices you want to test with. Finally create a provisioning profile (remember to do this in the "Distribution" tab). If you have done things correctly you should have a certificate in your default keychain. Here is what I see: 
    (1) Apple's Certificate (2) Your Certificate (3) Your private key
    Make sure you have something like this in your user's Keychain
  6. Another assumption is that you use the latest version of Phonegap (currently v2.2) which provides handy command line tools to create iOS application shell.  I used:
./path/to/cordova-ios/bin/create /path/to/my_project com.example.my_project
Now before we start the real fun, let's explain a couple of caveats for working with Jenkins in this context:
  1. Jenkins jobs run as the jenkins user (created by the Jenkins installer), your jenkins user doesn't really know about the Keychain that you have created. We will create a different Keychain accessible by your jenkins user.
  2. Same goes to provisioning profiles, we will need to install the distribution provisioning profile for the jenkins user. 
  3. Code Signing for development and distribution is done with different provisioning profiles, from time to time, developers override the project settings so the profile (the Xcode UI really sucks!)  is configured wrongly. For this end we will add a script that makes sure we use the right profile.
Now let's start the real fun:
  1. Export the user's Keychain (usually under /Users//Library/Keychains) and import it into new Jenkins' Keychain (usually under /Users/Shared/Jenkins/Library/Keychains):
    user$ security default-keychain # retrieve the user's default keychain 
    user$ security export -k <user-keychain> -f pkcs12 -o <output-file-name>
    user$ sudo su jenkins # switch to jenkins account
    jenkins$ security create-keychain -P  <jenkins-keychain> 
    jenkins$ security import <exported-file-name> -k <jenkins-keychain> -f pkcs12 -A 
    jenkins$ security default-keychain -s <jenkins-keychain>  
  2. Copy your distribution provisioning profile to jenkins' user profiles directory /Users/Shared/Jenkins/Library/MobileDevice/Provisioning Profiles/
  3. Create a Jenkins job using the xcode plugin
    Fetch your code from git then add a build step which executes the xcode plugin build

    Remember that you should fill in these parameters
    • Target
    • Configuration
    • Project directory
    • output directory
    • Build IPA
    • Provisioning profile - direct to your profile
    • Unlock the keychain and provide the Keychain and its password

  4. In your job you will need to make sure that the right profile is used during the execution, I had to use some search and replace tricks. For this end I first find the provisioning profile's hash (a 28 hex-decimal hash):
    cp </path/to/.xcodeproj>/project.pbxproj  </path/to/.xcodeproj>/project.pbxproj.bak

    sed 's/PROVISIONING_PROFILE\ =\ "[^\"]*"/PROVISIONING_PROFILE\ =\ "<Profile Hash>"/g' </path/to/.xcodeproj>/project.pbxproj > </path/to/.xcodeproj>/project.pbxproj.pro1

    sed 's/\"PROVISIONING_PROFILE\[sdk=iphoneos\*]\"\ =\ "[^\"]*"/\"PROVISIONING_PROFILE\[sdk=iphoneos\*]\"\ =\ "<Profile Hash>"/g' </path/to/.xcodeproj>/project.pbxproj.pro1 > </path/to/.xcodeproj>/project.pbxproj.pro2

    mv </path/to/.xcodeproj>/project.pbxproj.pro2 </path/to/.xcodeproj>/project.pbxproj
  5. Distribute to external enterprise site - this one worth a separate post

8 comments:

D said...

Hi Roy,

I don't get the point at: security import -k /i> -f pkcs12 -A

What is >/i> ?

What I tried as jenkins-user:

bash-3.2$ security import -k ~/Library/Keychains/keychain -f pkcs12 -A
security: Error reading infile -k: No such file or directory
bash-3.2$ security import -k ~/Library/Keychains/keychain/i -f pkcs12 -A
security: Error reading infile -k: No such file or directory
bash-3.2$ security import -k <~/Library/Keychains/keychain>/i> -f pkcs12 -A
bash: /i: Permission denied

I hope you can help me.

Cheers!

Roy Ganor said...

the extra characters are a typo, I've removed it from the post.

You should make sure that you run this command as the jenkins user and provide the right path of the previously exported file

D said...

I'm stuck.. This is what I exactly did, do you manage to see what I am doing wrong?

bash-3.2$ security default-keychain
"/Users/ds/Library/Keychains/login.keychain"

bash-3.2$ security export -k /Users/ds/Library/Keychains/login.keychain -f pkcs12 -o /tmp/login.keychain
...29626 bytes written to /tmp/login.keychain

bash-3.2$ chmod 777 /tmp/login.keychain

bash-3.2$ whoami
ds

bash-3.2$ sudo su jenkins

bash-3.2$ whoami
jenkins

bash-3.2$ security create-keychain -P /Users/Shared/Jenkins/Library/Keychains/login.keychain

bash-3.2$ security import -k /tmp/login.keychain -f pkcs12 -A
security: Error reading infile -k: No such file or directory

bash-3.2$ ls -lah /tmp/
drwxrwxrwt 9 root wheel 306B 11 dec 20:31 .
drwxr-xr-x@ 6 root wheel 204B 10 jul 20:31 ..
-rwxrwxrwx 1 ds wheel 29K 11 dec 20:30 login.keychain

bash-3.2$

Roy Ganor said...

the format of the import command is import inputfile [-k keychain] [-t type] [-f format] [-w] [-P passphrase]

-k Target keychain to import into

D said...

I love you! ;)

I changed the command for importing the keychain into:

security import /tmp/login.keychain -k /Users/Shared/Jenkins/Library/Keychains/login.keychain -f pkcs12 -A

Unknown said...

This post was really helpful for getting iOS builds to work. I have several applications building, but my phonegap projects fail with the error below

'Cordova/CDVViewController.h' file not found

I can build from xcode on the jenkins server successfully though. This appears to just be a build from jenkins/command line issue with projects using phonegap.

I wonder if you could explain a little more about what was required for installing phone gap command line tools on your jenkins server?

Unknown said...

This post was really helpful for getting iOS builds to work. I have several applications building, but my phonegap projects fail with the error below

'Cordova/CDVViewController.h' file not found

I can build from xcode on the jenkins server successfully though. This appears to just be a build from jenkins/command line issue with projects using phonegap.

I wonder if you could explain a little more about what was required for installing phone gap command line tools on your jenkins server?

Unknown said...

I found your post really helpful for the keychain and provisioning profiles... but i'm stuck on the build

I can build and archive the projects from xcode on my jenkins machine, but I can't build from command line. It throws an error unable to find cordova files.

You mentioned you ran the cordova create command line on the jenkins server - i have not done this - can you explain this in more detail? I'm not sure why or where I would need to do this on my jenkins build