Roll your own Testflight 1

Ad-Hoc testing on the iphone platform is kind of a pain. It got a lot easier with Ad-Hoc web distribution, ala Testflight, but, I wanted to build a script that I could use to create a build that would be uploaded to my own web server, right from the build in Xcode. This has taken me a little while to figure out, but I found success. Now I hit ‘Archive’ and my testers can immediately download my new build using their mobile browser. Easy like it should be.

First you might need to know a little about provisioning (required for any device install). You’re going to need to create a provisioning profile for ad-hoc distribution and add all the device ids (UDID) to it in order for anyone to access your build. You’ll set Xcode to use the profile for your build. If you have any questions about how this is done, you can see how to do it here.

Now, a few words about Ad-Hoc, or as Apple calls it, Enterprise distribution. In Xcode 4, you can create the necessary files for a OTA Ad-Hoc distribution by choosing an iOS device, choosing the archive option from the Product Menu, going to the Organizer Window, pressing the ‘Share’ button with the archive selected, choosing .ipa or app store archive, and then ticking the ‘Save for Enterprise Distribution’ box and filling out the required information.

Xcode is going to generate a plist for you, this is the manifest. It tells the device a bunch of things about the .ipa file, including the bundle identifier, the name of the app, the version number, the url for the actual .ipa file, etc. This .plist file is what you will link to on your website. Also, an .ipa archive is a special kind of archive used for this enterprise distribution and for app store submission.

Once you’ve generated your .ipa and .plist you just need a web server and the right kind of link in order to have your users download it right from their phone. We are going to replicate what Xcode does in our build script, upload it automatically on the command line using ftp, and have a PHP script auto generate our new link for us. Easy-peasy.

The whole script is run in a build phase, if you go to Xcode, click the project container, go to the Build Phase tab and add a new run script phase, this is where you’ll paste in this script. There are some things about using a build phase that aren’t ideal, but for now, it seems to be the only way to do it. If anyone knows better, please leave comments.

Download the files here.

So, on to the script,

We’ll look at the second part first, because it’s simpler,

#!/bin/bash

#customize the following values for your environment
username=”username”
password=”password”
urlToUpload=”ftp.ftpsite.com:21/myappsdirectory/”

ftp -u ftp://$username:$password@$urlToUpload $1.ipa
ftp -u ftp://$username:$password@$urlToUpload $1.plist

This second part simply uploads the two files to the appropriate ftp site. I tried to put this into the main script, but I had errors that creating a secondary script seem to solve. If anybody can tell me why that is . . . I’d love to hear it.

On to the script, I can explain each part,

if [“${PLATFORM_NAME}” -eq “iphoneos”]
then

This just makes it so that it only runs if we are doing a device build, if we’re are running in the simulator, the script skips everything.

/usr/bin/codesign -s “${CODE_SIGN_IDENTITY}” “${TARGET_BUILD_DIR}/${TARGET_NAME}.app”
cd “${TARGET_BUILD_DIR}”

This command signs the code with our identity. This line is necessary because we are running the script in a build phase. It would be better to do a post action script, which would mean we could use the .app file that’s already signed, but because Xcode 4 doesn’t expose build variables in it’s pre and post action scripts, that isn’t an option. I hear rumors that they will fix this at some point. Hopefully soon.

#set up paths, and values in plist
#customize the next two lines
pathToFTPScript=”/Users/${USER}/Coding/Scripts/UploadBuild.command”
urlForIpa=”http://myurl/apps/${TARGET_NAME}.ipa”

#these values remain constant
applicationPath=”${TARGET_BUILD_DIR}/${TARGET_NAME}.app”
bundleVersion=$(defaults read “$applicationPath/Info” CFBundleVersion)
bundleIdentifier=$(defaults read “$applicationPath/Info” CFBundleIdentifier)

These first two lines need to be customized. The first is the location of the second script shown above. The second is the network location of the .ipa file. You will specify in the second script where you put the .plist and .ipa files, this value needs to be placed in the urlForIpa variable as this is what will be put into the .plist file.

The next three lines set up the rest of the values for the .plist by looking at the project settings for Bundle Identifier and Bundle Version.

#package the ipa
rm -rf Payload
rm -f “${TARGET_NAME}”.*.ipa
mkdir Payload
cp -Rp “${TARGET_NAME}.app” Payload/
if [ -f “${PROJECT_DIR}”/iTunesArtwork ] ; then
cp -f “${PROJECT_DIR}”/iTunesArtwork Payload/iTunesArtwork
fi
zip -r “${TARGET_NAME}.ipa” Payload

This just creates the .ipa. The .ipa file is just the .app, placed in a directory called Payload and zipped. If there’s an iTunesArtwork file, it’s put into Payload as well.

#create the plist

echo “<?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>$urlForIpa</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>$bundleIdentifier</string>
<key>bundle-version</key>
<string>$bundleVersion </string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>${TARGET_NAME}</string>
</dict>
</dict>
</array>
</dict>
</plist>” > “${TARGET_NAME}.plist”

These lines create the .plist.

#call upload script
sh $pathToFTPScript “${TARGET_NAME}”
fi

And finally, we call our second script. This second script just handles the uploading of the files to the web server.

The only thing left, is the php script that will generate a new link for us, each time we upload a new file set. Here’s the script.

<!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN” “http://www.w3.org/TR/html4/loose.dtd”>
<html>
<head>
<title>Install iOS App</title>
<style type=”text/css”>  li { padding: 1em; }  </style>
</head>
<body>
<ul>
<?php
if ($handle = opendir(‘.’)) {
while (false !== ($file = readdir($handle))) {
if ($file != “.” && $file != “..”) {
if (false !== strpos($file, “.plist”)) {
$n = substr($file, 0, strlen($file) – 6);
echo “<li><a href=\”itms-services://?action=download-manifest&url=http://myideasareimportant.com/apps/$file\”>Install $n</a></li>”;
}
if (false !== strpos($file, “.apk”)) {
$n = substr($file, 0, strlen($file) – 4);
echo “<li><a href=\”http://myideasareimportant.com/apps/$file\”>Install $n – Android</a></li>”;
}
}
}
closedir($handle);
}
?>
</ul>
</body>
</html>

This simple php script just looks at the current directory for .plist and .apk (android) files and creates a link for each one.  It’s really bare bones, but it does the job.

One thing to note, myurl.com/apps/ should be changed in both places to be your web location.  The links for the iphone apps have the following custom structure, <a href=”\&quot;itms-services://?action=download-manifest&amp;url=http://myurl.com/apps/$file\&quot;”>, where $file is replaced bye the .plist file for that app.

Upload the php to your webserver, paste the script into the last build phase of your project, create the upload script and you are on your way to easy ad-hoc distribution.

You can download all three files can be downloaded here.

One comment on “Roll your own Testflight

  1. Reply Shantanu Sep 3,2013 6:59 am

    That was extremely useful!! Thank you very much!!

    I just had a little question, is it possible to read the bundle identifier and version etc out of the ipa file? So on the web server, if someone uploads an ipa file then the php script will read be able to read that info from it?

    Is it possible? If yes, could you please let me know how to go about it?

    Thanks again!

Leave a Reply