Automated Ad-Hoc Beta iPhone App Publishing

A while back, I came across Jeffrey Sambells post about iOS Wireless App Distribution, thought “that’s cool, I could automate most of that” and then promptly forgot about it. A little after this, Hunter Hillegas introduced iOS Beta Builder which I also thought “cool, I could extend that to do some of the automation I thought about” and then forgot about it again.
BetaBuilder-TDO App showing ABC of the Sea metadata

BetaBuilder-TFO App


Ad Hoc app distribution

I already had a small shell script I run to make my .ipa files which saves me from the tediousness of “Build and Archive” and then saving the archived file – if I have to click something, it loses my attention. I already build iOS projects with an extra build configuration called “Ad Hoc” that signs with the current Ad-Hoc distribution profile. Beta Builder nicely took the .ipa (which is really just a zip file), discovered the contents and produced a handful of useful things:

  • The manifest file, that iOS 4.0 and newer can use to find the .ipa file.
  • A zip file containing the .ipa file and a copy of the distribution profile, for users that need to use iTunes to load Ad-Hoc builds.
  • A nice index.html to link to all of the above with brief instructions.

Initially I setup Beta Builder to deliver the files it built to a local directory and simply rsync‘ed the structure to my origin HTTP server. That was nice, but it involved too much clicking for my liking. Hunter had hinted that the source would appear on GitHub sometime… but again, I forgot all about it until he posted version 1.0.1 and I happened to want to distribute another build of ABC of the Sea. I was motivated. What I wanted to achieve was to be able to build an Ad-Hoc target in Xcode and then run one script to publish the package. I love this stuff. To get there, I decided I needed:

BetaBuilder HTML page for ABC of the Sea beta release

BetaBuilder generated HTML

  • My existing script that builds an .ipa from the build directory.
  • A non-interactive version of Beta Builder, driven from the command line.
  • For Beta Builder to read more keys from the projects Info.plist file, in particular, to differentiate between the application version and build number.
  • To link to the README.txt file on the HTML page that Beta Builder generates.
  • To have a more useful name for the legacy zip file it generates.
  • And then rsync the resulting files to my origin server.

I made a copy of Beta Builders source tree in a local SVN repository and hacked away (sometime I’ll create a branch and commit it back to GitHub). I won’t belabor over what I changed (diffs and such are attached) but it suffices to say that I can publish my beta releases with one command, sweet.

The scripts

Aside from my hacked version of Beta Builder, there are three shell scripts that run together.

  • <PROJECT_DIRECTORY>/Scripts/package-app.sh which knows the project and target names to use for a given project.
  • $HOME/bin/package-app that does most of the grunt work.
  • $HOME/bin/sync-iosbeta that copies my local iosbeta directory tree to the origin HTTP server.

The first of these is very simple. Its responsibility is to validate the only parameter it receives (the target configuration name, Debug, Release, Ad Hoc), ensure the build is uptodate (call xcodebuild) and then package it all up.

<PROJECT_DIRECTORY>/Scripts/package-app.sh
1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
# $Id: package-app.sh 335 2010-09-03 00:07:00Z chrisy $
conf="$1"
[ "$conf" = "AdHoc" ] && conf="Ad Hoc"
if [ "$conf" != "Debug" -a "$conf" != "Release" -a "$conf" != "Ad Hoc" ]; then
        echo "ERROR: Invalid configuration. Use Debug, Release or Ad Hoc."
        exit 1
fi
cd $(dirname $0)/../ || exit 1
xcodebuild -configuration "$conf" || exit 1
exec $HOME/bin/package-app 'ABC of the Sea' 'ABC of the Sea' "$conf"

The real work is done in package-app. The primary function is to build a ZIP file crafted in the correct manner. This is simply a copy of the targets build directory but prefixed with Payload/ in the path. The resulting file is often suffixed with .ipa, but .zip works just as well with all the tools I have tried it with. I since tweaked the script to call my modified Beta Builder and distribute the results, and my tweaks to Beta Builder include steps to name the package as a .ipa, just in case.

This is not the cleanest of shell scripts, but it is functional:

$HOME/bin/package-app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/bin/sh

project=$1
app=$2
type=Release
extra=

if [ -z "${project}" -o -z "${app}" ]; then
        echo "Usage:   $(basename $0) <project name> <target name> [configuration]"
        echo "example: $(basename $0) FivePeeEmm 5somewhere"
        echo "configuration defaults to 'Release'"
        exit 1
fi

if [ ! -z "$3" ]; then
        type=$3
        extra="-$3"
fi

base="${HOME}/Documents/iPhone Projects/${project}"

if [ ! -d "${base}" ]; then
        echo "ERROR: Can't find project \"${project}\" at \"${base}\""
        exit 1
fi

appdir="${base}/build/${type}-iphoneos"

if [ ! -d "${appdir}/${app}.app" ]; then
        echo "ERROR: Can't find app \"${app}\" at \"${appdir}/${app}.app\""
        exit 1
fi

mkdir -p $HOME/Applications || exit 1

# Make a copy of the build into the Payload directory
payloaddir="${base}/build/Payload"
payload="Payload"
rm -rf "${payloaddir}"
mkdir -p "${payloaddir}"
cp -rp "${appdir}/" "${payloaddir}/"

cd "${payloaddir}/.."
bindir="${payload}/${app}.app"

# Get build and release versions
bundle_id=$(plutil -convert xml1 -o - - < "${bindir}/Info.plist"  |
        grep -A 1 CFBundleIdentifier |
        sed -e 's/<[^>]*>//g' -e 's/[        ]//g'| tail -1)
bundle_short=$(echo "${bundle_id}" | sed -Ee 's/^.*\.(.*)$/\1/')
release_ver=$(plutil -convert xml1 -o - - < "${bindir}/Info.plist"  |
        grep -A 1 CFBundleShortVersionString |
        sed -e 's/<[^>]*>//g' -e 's/[      ]//g'| tail -1)
build_ver=$(plutil -convert xml1 -o - - < "${bindir}/Info.plist"  |
        grep -A 1 CFBundleVersion |
        sed -e 's/<[^>]*>//g' -e 's/[   ]//g'| tail -1)

echo "Packaging project \"${project}\" target \"${app}\""
echo "  Bundle ID \"${bundle_id}\" (${bundle_short}) version ${release_ver} build ${build_ver}..."

zip="$HOME/Applications/${app}-${release_ver}-${build_ver}${extra}.zip"
zip=$(echo "${zip}" | sed -e 's/  */_/g')

year=$(date +%Y)
rm -f "${zip}"
zip -Xoyrz "${zip}" "${bindir}" << EOT | grep -v 'enter new zip file comment'
Project: ${project}
App: ${app}
Release: ${release_ver}
Build: ${build_ver}
(c) ${year} Chris Luke

.
EOT

zip -T "${zip}" || exit 1
unzip -tq "${zip}" || exit 1
echo "Done. Package is ${zip}"

if [ "${type}" = "Ad Hoc" ]; then
        # One more step - package it up for iosbeta distribution!
        odir="$HOME/Documents/iosbeta/${bundle_short}-${build_ver}"
        ourl="http://example.com/${bundle_short}-${build_ver}"
        echo "Distributing Ad Hoc release into ${odir}..."
        mkdir -p "${odir}" || exit 1
        if open "$HOME/Documents/MacOSX Projects/BetaBuilder TFO/build/Debug/BetaBuilder.app" --args \
                -i "${zip}" -o "${odir}" -u "${ourl}" -r "${base}/README.txt" ; then
                $HOME/bin/sync-iosbeta
        else
                echo "BetaBuilder encountered an error. Check system logs."
                exit 1
        fi
fi

BetaBuilder TFO

My hack to add command-line driven options to BetaBuilder simply uses getopt(3) on the argc/argv parameters in main() and if we recognise any parameters then it never calls UIApplication to fire-up the GUI. For example, output from -h:

1
2
3
4
5
6
7
8
Usage: /Users/chrisy/Documents/MacOSX Projects/BetaBuilder TFO/build/Debug/BetaBuilder.app/Contents/MacOS/BetaBuilder [options]
If no options are specified, the app runs in interactive mode.

 -h            This message
 -i <file>     The input file (.zip or .ipa) (required)
 -o <dir>      The output directory (required)
 -u <url>      The URL of the output directory (required)
 -r <file>     The README.txt file to include (optional)

However, running the binary directly like this doesn’t give the app access to its bundle, and that’s where the template index.html lives. That is why package-app uses open to run the program (however, this causes NSLog() output to all go to the system logs, not stderr).

Running it

And, for kicks, here’s a snippet of sample output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
bean:~ chrisy$ cd Documents/iPhone\ Projects/ABC\ of\ the\ Sea/
bean:ABC of the Sea chrisy$ Scripts/package-app.sh AdHoc

=== BUILD NATIVE TARGET ABC of the Sea OF PROJECT ABC of the Sea WITH CONFIGURATION Ad Hoc ===
Check dependencies
CodeSign "build/Ad Hoc-iphoneos/ABC of the Sea.app"
    cd "/Users/chrisy/Documents/iPhone Projects/ABC of the Sea"
    setenv PATH "/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin:/Developer/usr/bin:/Users/chrisy/bin:/opt/subversion/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/X11/bin:/Developer/usr/bin:/Developer/usr/sbin"
    setenv _CODESIGN_ALLOCATE_ /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/codesign_allocate
    /usr/bin/codesign -f -s "iPhone Distribution: Chris Luke" "--resource-rules=/Users/chrisy/Documents/iPhone Projects/ABC of the Sea/build/Ad Hoc-iphoneos/ABC of the Sea.app/ResourceRules.plist" --entitlements "/Users/chrisy/Documents/iPhone Projects/ABC of the Sea/build/ABC of the Sea.build/Ad Hoc-iphoneos/ABC of the Sea.build/ABC of the Sea.xcent" "/Users/chrisy/Documents/iPhone Projects/ABC of the Sea/build/Ad Hoc-iphoneos/ABC of the Sea.app"

/Users/chrisy/Documents/iPhone Projects/ABC of the Sea/build/Ad Hoc-iphoneos/ABC of the Sea.app: replacing existing signature
Validate "build/Ad Hoc-iphoneos/ABC of the Sea.app"
    cd "/Users/chrisy/Documents/iPhone Projects/ABC of the Sea"
    setenv PATH "/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin:/Developer/usr/bin:/Users/chrisy/bin:/opt/subversion/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/X11/bin:/Developer/usr/bin:/Developer/usr/sbin"
    setenv PRODUCT_TYPE com.apple.product-type.application
    /Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/Validation "/Users/chrisy/Documents/iPhone Projects/ABC of the Sea/build/Ad Hoc-iphoneos/ABC of the Sea.app"

** BUILD SUCCEEDED **

Packaging project "ABC of the Sea" target "ABC of the Sea"
  Bundle ID "org.flirble.abcsea" (abcsea) version 1.0.0 build 12...
  adding: Payload/ABC of the Sea.app/ (stored 0%)
  adding: Payload/ABC of the Sea.app/_CodeSignature/ (stored 0%)
  adding: Payload/ABC of the Sea.app/_CodeSignature/CodeResources (deflated 64%)
  adding: Payload/ABC of the Sea.app/a-ipad.png (stored 0%)
...
  adding: Payload/ABC of the Sea.app/z-iphone.png (stored 0%)
  adding: Payload/ABC of the Sea.app/zooplankton.html (deflated 51%)
test of /Users/chrisy/Applications/ABC_of_the_Sea-1.0.0-12-Ad_Hoc.zip OK
No errors detected in compressed data of /Users/chrisy/Applications/ABC_of_the_Sea-1.0.0-12-Ad_Hoc.zip.
Done. Package is /Users/chrisy/Applications/ABC_of_the_Sea-1.0.0-12-Ad_Hoc.zip
Distributing Ad Hoc release into /Users/chrisy/Documents/iosbeta/abcsea-12...
Syncing iOS beta distribution...
building file list ...
13 files to consider
abcsea-12/
abcsea-12/ABC_of_the_Sea-1.0.0-12-Ad_Hoc.ipa
      32.88M 100%  119.64MB/s    0:00:00 (xfer#1, to-check=4/13)
abcsea-12/ABC_of_the_Sea-1.0.0-12-Ad_Hoc.zip
      32.62M 100%   59.26MB/s    0:00:00 (xfer#2, to-check=3/13)
abcsea-12/README.txt
       7.63K 100%   14.20kB/s    0:00:00 (xfer#3, to-check=2/13)
abcsea-12/index.html
       2.01K 100%    3.75kB/s    0:00:00 (xfer#4, to-check=1/13)
abcsea-12/manifest.plist
         763 100%    1.42kB/s    0:00:00 (xfer#5, to-check=0/13)

sent 105.70K bytes  received 68.99K bytes  116.46K bytes/sec
total size is 131.00M  speedup is 749.90
bean:ABC of the Sea chrisy$

Attachments

All I need do now is email the mailing-list I have of beta testers to let them know build 12 is up and where to find it – and I’ll probably automate that, too!

Related Reading

Shell Scripting Tutorial
The iPhone Book: Covers iPhone 5, iPhone 4S, and iPhone 4 (6th Edition)
Learning iOS Programming: From Xcode to App Store
iPhone: The Missing Manual
The Bourne Shell Quick Reference Guide
Programming in Objective-C (5th Edition) (Developer's Library)
Did you like this? Share it:

Tags: automation, BetaBuilder, Hunter Hillegas, iOS, iPad, iPhone, Jeffrey Sambells, MacOS X, Programming, shell script, Xcode
Posted in: iOS, Technology
Both comments and pings are currently closed.

2 Comments

  1. For what it’s worth, I have branched iOS-BetaBuilder on GitHub. You can see the branch at http://github.com/chrisy/iOS-BetaBuilder .

  2. [...] couple of weeks ago I posted about automating Ad-Hoc publishing using some simple shell scripting and a modified version of the BetaBuilder utility by Hunter [...]