With the launch of iOS6 and the corresponding Xcode 4.5 update, Apple quietly dropped support for producing binaries compatible with ARMV6 devices. This means that if you want to build an app that makes use of the new APIs introduced in iOS6, your app can’t also run on the original iPhone, the iPhone 3G or the first two generations of the iPod Touch. Developers can still use older versions of Xcode to produce binaries that will work on all devices, but this means losing support for APIs introduced in iOS6, including full 4″ screen support. Further, as of May 1st Apple will stop approving apps that don’t fill the iPhone 5′s screen.
I’m not going to bury this lede any further: it is entirely possible to build apps supporting all iOS hardware in Xcode 4.5+, and I’m going to explain how to do it.
I’m sure many developers wonder why this is even worth kerning vector letterforms over – who cares about a circa-2008 iPhone 3G? I don’t expect many of your sales to be coming from these older devices (which are still kicking around in the millions, mind you), but you may be in the same boat we’re in with Pano – we have a user-base numbering in the hundreds of thousands, stretching back nearly five years, and we don’t want to leave these users out in the cold. Further to that, the decisions we make about legacy support have far reaching impacts on how quickly older devices are forced into obsolescence, and I’d argue that we have a responsibility to appreciate the real cost of these devices. In our case, there were some social features we wanted to add to our app, and these depended heavily on iOS6 APIs. We’ve worked hard to fight target-version creep, and we don’t give up the ghost easily.
Some technical background: while all iOS devices (to date) run ARM processors, these processors have used three ARM architecture variants – ARMV6, then ARMV7 and now ARMV7s. While these architectures are backward compatible – your ARMV7s iPhone 5 can run an older ARMV6 binary – your iPhone 3G can’t run a binary that only contains ARMV7s machine code. Apple has experience transitioning between different processor architectures on their desktop machines, first from 68k to PowerPC, then from PowerPC to Intel. The solution to this architecture-compatibility problem is to produce a fat binary; that is, a single binary that contains multiple compiled versions of your code, one each for the architectures your application will support. On OS X and iOS, this is accomplished using a tool called lipo that slices up binaries and performs a bunch of apt-surgical-metaphor-goes-here functions on them: stripping architectures out and stitching new architectures in.
For most developers, this happens unseen and under the hood during Xcode’s build process. Prior to the iPhone 5, modern versions of Xcode were using lipo to produce fat iOS binaries that natively supported ARMV6 and ARMV7. Starting in Xcode 4.5, this shifted to ARMV7 and ARMV7s, and Xcode dropped support for producing ARMV6 machine code altogether.
The solution to this problem is pretty simple – conceptually, at least; your Xcode build process needs to produce an ARMV6 image for your application, then stitch it into the application binary, producing a fat binary that includes machine code for ARMV6, ARMV7 and ARMV7s.
In practice, there are two parts to this process. The first is to produce an application that will run appropriately on all of the devices and API levels you plan to support. I’m not going to go into great detail here, as it’s an extension of what we’ve been doing for years to conditionally support APIs where they’re available. Xcode is still remarkably aloof when it comes to API awareness, so you’ll be well served by something like Ivan Vasic’s venerable Deploymate, which is a development tool expressly designed to check your API usage against your API target level and highlight potential problems. You’ll also want to #ifdef out any code that is iOS6 dependent – something like #ifndef ARMV6_ONLY and #endif should surround any code blocks that the legacy Xcode compiler would choke on (remember, it knows nothing about iOS6). In my experience, weak-linking frameworks that were introduced in iOS6 will still cause some headaches when they’re part of your project, so we’ll keep Xcode in the dark here. Remove iOS6-exclusive frameworks (Social.framework, I’m looking at you) from the Linked Frameworks and Libraries section of your target’s summary pane and instead add them to your Other Linker Flags build setting in the form “-framework <framework-name>”; in my case that looked like this:
The second part of this process is where the dance really begins: we have to compile that extra binary image and stitch it into our app bundle. The earliest incarnations where people got this working involved jumping back and forth between two versions of Xcode, but I’m happy to report that we can automate things and have it all run from within Xcode 4.5+. ARMV6 support has been dropped entirely from within the build chain in these newer Xcode versions, however, so we’ll still need to install an older version of Xcode beside our current install. I’d suggest using Xcode 4.4.1 – this is the last release to support ARMV6 and it’s downloadable from Apple’s developer site.
You’ll want to duplicate your build configuration (in your Project’s Info pane) to create an ARMV6-specific one; if you’re feeling creative you might call it Release-armv6. In your target’s build settings, you’ll want to set both the Architectures and Valid Architectures for your ARMV6 build configuration to “armv6″, while the rest should be set to armv7 armv7s.
In order for the legacy version of Xcode to skip over your ifndef’ed code blocks, you’ll need to also add a compiler flag to Other C++ Flags and Other C Flags in the same build settings pane; assuming your preprocessor blocks are looking for the ARMV6_ONLY token, your flag will be -DARMV6_ONLY
Your cupcakes should be beginning to rise, and they should look something like this:
Next we’re going to add a run script build phase to our target that uses the c shell (shell: /bin/csh). Our run script will do the following: (1) compile an ARMV6 version of our binary using our ARMV6 build configuration; (2) modify the app’s plist to lower the minimum iOS version (Xcode 4.5+ won’t write out a version below 4.3), then; (3) verify that our final binary includes our desired architectures.
I’ve adapted my run script from one posted here; mine is posted as a gist here, and it includes additional code to do basic sanity checking on your build – following the lipo process, we check to verify that all three desired architectures are present in the binary and we fail the build if they aren’t. You’ll want to ensure that you’ve configured your build appropriately for your own setup; the build script produces a useful log file that may be helpful if you need to debug a failed build.
I want to note here that Xcode uses its own version of lipo which is distinct from the binary that ships with OS X – you will will want the fully qualified path to Xcode’s own version if you’d like to verify your binary’s architectures yourself; that path is:
Further, be aware that your build script will wipe Xcode’s $ARCHIVE_PATH environment variable, meaning that archive builds will fail to automatically archive. The .xcarchive bundle will, however, be generated, meaning that you can manually archive these files yourself. Xcode’s archive process is quite opaque, but I’ve determined that the archive list in the Xcode Organizer is populated by scanning for a special Info.plist file inside of your .xcarchive bundle. This plist file won’t be generated during our build process, so I’ve written a script that you may want to add as a post-action to your scheme’s Archive settings. My script (reproduced here) grabs a template Info.plist (I’d suggest snagging one from a previous application archive), then customizes it for your archived build and places it into your .xcarchive bundle.
… and there you have it – triple-architecture fat binaries, hot and fresh out of Xcode 4.5.
– I’d like to specifically acknowledge a whole bunch of Stackoverflow contributors, including Mike and Jerome, for their work, which I built on for the above post. Also, thanks to Justin Williams for providing a minimally-helpful-but-technically-correct suggestion when I was trying to figure out how the Xcode Organizer searches for .xcarchive bundles.