I’ve written about building iOS applications with
Hudson Jenkins, but until recently there hasn’t been a convenient way of getting those applications to your testers. Of course the most important part of your build output will be the app bundle you send to Apple’s iTunes Connect web interface, but throughout your development cycle you’ll want to test your app. Sure you could build and deploy a debug build straight to your own personal device, but you get the most benefit from having other people beta test your app.
With recent releases of Xcode and the iOS SDK, Apple improved their AdHoc distribution support with two main enhancements:
- Mobile provisioning files can now be embedded in the App’s IPA itself, meaning you don’t have to maintain and update separate .mobileprovision files separately;
- A specially-formated manifest Plist file can be created that, when linked to properly, allows test devices to install new versions of your AdHoc app without needing to plug into a computer to sync the app using iTunes.
These improvements are huge, but require some changes to your build scripts and your Continuous Integration environment. I’d like to show you how to do this in your own installations, and show you some options for how to distribute your apps to your testers.
Building IPAs properly
In the past, if you wanted to create an AdHoc distributable binary, you could either zip up the MyAppName.app bundle directory and mobile provisioning files separately, or you could attempt to create an IPA manually to fake-out iTunes. Getting this right was difficult however since the contents of the app directory are signed, and any changes (e.g. trying to include an embedded mobileprovision or iTunesArtwork file) would break the codesign.
With recent iOS SDK releases Apple added a “Build And Archive” menu that builds an IPA and archives it for later use. This is fairly nifty if you are a one-man shop and don’t practice Continuous Integration, but for the rest of us this just won’t cut it. However, some adventurous people dug through Xcode’s command-line build output to see just what was happening when the “Build And Archive” command was generating the IPA, and here’s what they found:
xcrun -sdk iphoneos PackageApplication \
-o "output/path/to/MyApp.ipa" \
--sign "iPhone Distribution: My Company" \
If you run that on the command-line, lo’ and behold, you can generate properly-signed IPAs to your heart’s content. But there’s more! The interesting thing about this command is that it takes both the codesign name and certificate file as arguments while generating your IPA. There’s a subtlety to this that many people haven’t stumbled upon. Here it goes:
No matter what provisioning profile you use within your Xcode project file to build your apps, you can re-sign them later in your Continuous Integration environment!
This is a powerful point that you can really take advantage of while setting up your build scripts. As mobile provisioning files change, you only need to ensure you perform the final IPA creation with the proper profile.
Adding a statement above to your build script is fairly straight-forward. Here is a snippet of code I use in my own build scripts:
for config in $CONFIGURATIONS; do
provfile=$(eval echo \$`echo ProvisionFile$config`)
codesign=$(eval echo \$`echo Codesign$config`)
xcodebuild -activetarget -configuration $config build || failed build;
app_path=$(ls -d build/$config-iphoneos/*.app)
xcrun -sdk iphoneos PackageApplication \
"$app_path" -o "$OUTPUT/$ipaname" \
--sign "$codesign" --embed "$cert"
I then place some simple configuration into the “build.config” file, that looks like so:
CodesignDistribution="iPhone Distribution: Decaf Ninja Software"
CodesignRelease="iPhone Distribution: Decaf Ninja Software"
This is used by the script to determine, at runtime and on a per-artifact basis, which codesign and mobile provision file should be used. This means that if your provisioning profile changes (if you add or remove devices from an AdHoc profile, for example) you only have to check the new provisioning file into your SCM, submit to Jenkins, and wait for a new build. (Note: The settings starting with “OTA” will be used in the next section)
The great thing I discovered is that Apple’s iTunes Connect web interface accepts properly-signed IPA files as your final app submission, as long as they’re signed with your App Store mobile provisioning profile. This means if you build an IPA for both AdHoc and App Store configurations, that’s all you need.
Once you have an IPA generated with an embedded mobile provision profile, you can start distributing your apps to beta testers much easier. In my experience it’s difficult to find beta testers who will diligently put your app through its paces thoroughly enough to truly test your application’s limits. It makes matters even harder when you require users to: a) download an IPA, b) maybe download a new mobile provision profile, c) drag them into iTunes (in the right order), and d) sync their phone. You’ll be lucky at that point if they actually have the time to launch your app.
There are a few 3rd-party solutions (such as TestFlight or Hockey) that handle the nitty-gritty of getting Over-The-Air installs working to manage your beta testers and to push updates out to them, but most of the features they provide can be built yourself simply enough just by adding a little bit of creativity to your Jenkins build scripts. I’m going to work backwards, since that might be easier to follow along with.
Send email notifications to your testers
The screenshot to the right shows a sample email that my beta testers and I receive when a new build is successfully compiled. I use the Jenkins Email-ext plugin to customize my build result emails, and enable HTML output so that I can include custom links in it. Using this plugin allows you to easily send separate messages on success, failure, etc, and provides a number of variables/tags that can provide additional context to your testers. For example, I use the “$CHANGES_SINCE_LAST_SUCCESS” tag to show my testers what’s changed since the last build, using my SCM commit log messages.
Apple uses a special URL scheme to indicate that an app install should be performed. In the email screenshot, you’ll see that I have a link titled “Install beta build #32″. This is link to the following URL:
Clicking that link from an iOS device will tell the OS to download the application update manifest and, if it’s properly formulated, will prompt the user if they want to install the app.
Assuming the manifest is valid and the user taps “Install”, it behaves exactly as if the user purchased the app from the App Store. The device switches back to the home screen, and begins downloading the application.
Note: If your user’s device UDID isn’t included in the embedded mobile provision profile, the user will get a vague error about being unable to install the application.
Generating the update manifest
All of this assumes you’ve generated your update manifest to begin with. The format is pretty verbose, and has a few hard requirements for the information you need to supply. But honestly these are files you should have available to begin with. They include:
- Your company name
- The name of the application
- The version number that you want to report for this app
- Your 57×57 app icon
- Your 512×512 app icon
There is some other information you can optionally supply, but that is the minimum you need. All of those files need to be available on a web server the user will have access to. So if your Jenkins server is password protected, you might want to archive those files elsewhere. For my installation I use the Jenkins SCP plugin to archive the relevant files to a public “Beta” section of my website and reference the files from my app manifest.
I generate my app manifest from my build scripts, which looks something like this:
cp $WORKSPACE/$OTASmallIcon $OUTPUT/$fileprefix/Icon-57.png
cp $WORKSPACE/$OTALargeIcon $OUTPUT/$fileprefix/Icon-512.png
info_plist=$(ls *Info.plist | sed -e 's/\.plist//')
bundle_version=$(defaults read $WORKSPACE/$info_plist CFBundleShortVersionString)
bundle_id=$(defaults read $WORKSPACE/$info_plist CFBundleIdentifier)
cat << EOF > $OUTPUT/$otaname
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
The contents of the "output" directory are subsequently SCP'd to my beta server, and this manifest file is referenced in my Jenkins build success email.
The last step is to maintain an email mailing list of your beta testers and configure your Jenkins build job to notify that list when a successful build is finished. Your testers are then notified the moment any new changes are available, and with a single tap they can download and update to the latest build from anywhere. And your testers can see, based on your SCM changelog, what features may have changed and areas where they might need to focus their test efforts.
If you have any questions about this technique, please don't hesitate to leave comments below. I use this for my personal iOS application builds, as well as at work, and we've had great success with it, especially since it doesn't require the installation or configuration of any extra 3rd-party software.