header image

Automated iOS deployment

I blogged previously about the build system that we’ve written for Mini Metro; the idea is from just one command on the terminal, we can have any number of platforms built and deployed remotely without any further interaction. This has been working well for the desktop builds for months, but not for iOS.

We faced two problems getting the build system to support iOS properly. First, Unity’s build command doesn’t output an executable for iOS, only an Xcode project. Second, how can we deploy an iOS app from the command line?

Command-line Xcode build

The first step was the easiest to solve. Once Unity’s built your Xcode project, you’re just a couple of commands away from a packaged iOS build. We run the following command from the Xcode project folder:

xcodebuild -project Unity-iPhone.xcodeproj -scheme Unity-iPhone archive -archivePath [output.xcarchive] -configuration Release OTHER_LDFLAGS=“-framework GameKit” PROVISIONING_PROFILE=[provisioningProfileId] CODE_SIGN_IDENTITY=“[codeSigningIdentity]”

The tricky bit is (as always!) the provisioning profile. You need to specify the id of an Ad Hoc Distribution profile that you’ve got loaded into Xcode. The id is tricky to find; go in to Preferences, click View Details… for your team, right-click on the appropriate profile and select Show in Finder. What you’re after is the filename (it should be all hex, with a few dashes) without the .mobileprovision extension.

The code signing identity is of the form “App Developer (DUCNFCN445)”. If you don’t know yours off by heart, you can get a list of all entities with the command security find-identity -v -p codesigning. (Thanks, as always, to Stack Overflow.)

Also note the GameKit reference! Unity produces an Xcode build without GameKit included, so this line just ensures that it’s linked to. Took me a while to figure that one out. 😐

Assuming the stars align, that command will produce a signed archived build with the .xarchive extension. You can’t do much with it directly (most of this iOS voodoo is a mystery to me!), but with one more command we can extract a distributable package.

xcrun -sdk iphoneos PackageApplication -v [input.xcarchive]/Products/Applications/[appname].app -o [output.ipa]

And, Thor willing, you’ve got yourself a nice shiny .ipa file! From here, you can take the easy route and distribute this build through whatever means necessary to your dev or test team. Double-clicking it will load it into iTunes, and from there you can sync it on to any devices that were included in the provisioning profile it was packaged with.

Or …

Wireless deployment

This part is a fair chunk of work, and might not be worth it. It depends on how frequently you do builds, how difficult it is to get the builds to everyone on your team, and how decent your sysadmin skills are. For the record, my Linux-fu is barely adequate. I know just enough to follow step-by-step guides and get myself in trouble.

Apple have a good write-up on how the wireless deployment goes here. It’s a fairly straightforward process, but it took me a while to get it properly automated on the server.

The first step is actually getting the .ipa up to the server. We already have an SCP deployment target for our builds, so I simply hooked that target up to the iOS build configuration.

Second, we had to provide an SSL connection. As this isn’t a front-facing website I’d normally just use a self-signed certificate and click through the warnings, but you don’t have that option with wireless deployment. If the SSL certificate isn’t on the device’s Trust Store it will ignore the request. After a Twitter rant about how shady SSL certificate providers are, a number of helpful people pointed me to letsencrypt.com. Big thumbs up! This tool allows you to generate secure, trusted 90-day certificates nearly instantly. It’s still in beta, and only provides automatic certificate installation for Apache—as we use Nginx we had a bit more coding to do, but not much. It all worked straight out of the box.

Next, MIME types! As your server will be serving a .plist and .ipa file, you need to ensure that the server knows the correct MIME types for those extensions. I had to add them both to our Nginx configuration.

Now you need to provide access from the server to both the manifest .plist file describing the build (an example manifest is in the Apple docs linked above), and the build itself. You could simply copy Apple’s example manifest, edit the appropriate fields, and dump that on your server. If you know what the filename of the .ipa is going to be, then that’ll work fine—just strip out the optional checksum fields and you’re set. I wanted to support multiple named builds, so had to automate the .plist generation. My first solution was to specify a .php file as the manifest URL, and have that .php file generate the manifest. However Safari just didn’t like it; I think it needs the manifest file to have a .plist extension. I had a stab at redirecting the .plist link to execute the .php file but … err, I’m not sure what went wrong. Amateur sysadmin, remember! So in the end I took a less elegant approach. The process is now as follows:

  1. Build is uploaded to the server.
  2. I send an install URL to the dev team; this is something like https://test.com/install.php?build=[buildname]. Anyone who wants the new build opens the link on their iOS device.
  3. When install.php is executed, it finds the appropriately named .ipa on the server. It calculates the checksums and generates a manifest for the build, saving it to disk.
  4. install.php sends a Location HTTP header to redirect Safari to the manifest. This is the itms-services:// link documented in the Apple docs.

And it works! Stoked that I managed to bumble my way around the process, and that we have an iOS build deployment system that’s much more automated than I thought I’d be able to manage.