You thought I couldn't come up with the most boring topic of all time? You were sorely mistaken. On both accounts really. It's probably a case of Stockholm Syndrome or something, but I've kind of enjoyed learning some of the ins and outs of the Build Settings tab in Xcode.
This is kind of a specific topic, but it doesn't seem like it's talked about all that much. Sometimes you write code and that code gets turned into an app. Other times you write code for an open source project. Neato.
So what if you work at a company that wants to distribute an SDK to clients, but can't give them the source code directly? The answer is that you'll have to figure out how to package your code up into a pre-compiled framework and give them that.
Unfortunately, there's a lot of things that can go wrong along the way.
Building Pre-Compiled Frameworks That Don’t Break Client Apps
When you’re building a closed-source framework to distribute to clients, there’s a few extra things you have to think about that don’t necessarily come to the fore-front when you’re working on internal code or an open source project.
One thing that can be annoying at first is dealing with which architectures are included in the framework you’re giving out.
Just building a framework can be easy. First, make sure your target is actually a "framework" target. If this is the case, all you need to do is hit the play button in Xcode. Unfortunately, odds are good that you can’t just hand this framework over to your clients.
A pre-compiled framework is really just a Mach-O binary packaged up in a conventional folder structure. This folder also includes library headers and any required resources the framework might need.
As far as the binary goes, there are two families of architectures it can be compiled to target. An "architecture" means assembly code that is made for a specific type of CPU. First, you have the armvX set of architectures which relate to CPUs running on physical devices. Then, you have the i386, x86 and x86_64 architectures which are binaries built to run on a simulator.
The rub you’ll immediately run into is that your framework will only include the architectures of the device that was selected in Xcode at build time. This means that if you had a simulator selected, then your framework will include simulator architectures and the client’s app will only be runnable in the simulator. In this case, the client won’t even be allowed to submit their app to the App Store!
Same goes for building to a device. Give them a framework with no simulator symbols and you’ll get complaints when your client goes to try something out in the simulator.
Debug vs Release Mode
The next problem you might run into is giving them a framework that wasn’t built in release mode. This won’t be immediately obvious since they’ll be able to build and test their app locally just fine. The problem comes when their app is once again rejected by iTunes Connect. This time the problem will be that they're including Debug Symbols.
In general, release mode means optimizations have been turned on and all apps released to the App Store need to be built in release mode.
How Not to Jack Things Up
Since your goal is to get your framework into your client’s app without changing their mind about this relationship, you’re gonna want to try to avoid these mistakes.
Lucky for you, doing so can be reasonably straightforward, even if it does take a bit of extra thought.
Building a Fat Binary
To solve the problem where your client can only build for simulators or real devices, you can create what’s called a “fat” binary. It’s fat because instead of just including one set of architectures you can shove them all into one big framework.
You can do this at the command line using the xcodebuild and lipo commands.
1. First, build a version of the framework for devices. You don’t have to specify a device since the “Generic Device” option is the default.
2. Next, do the same thing but specify one of your installed simulators using the -destination argument.
3. Finally, you can take the two versions of your framework and mash them together into the final product.
The lipo command is a little bit harder to parse. First, you’re telling it that you want to create new binary and specifying the name with whatever comes after the -output flag.
Then you provide the paths to the two binaries that you’re trying to mash together.
A couple things to keep in mind is that the lipo command just creates a binary. You’ll want to make sure <build-location>/<framework-name>.framework/ already exists and that it includes the necessary headers and extra resources that were automatically included in the two frameworks you created using the xcodebuild command.
Also, notice we did specify that we wanted to build ‘Release’ versions of the frameworks in our xcodebuild commands. This means our second problem is taken care of as well.
The Problem Our Solution Creates
Like all good software fixes, this one creates one new problem in the process of fixing our original problems.
Giving them a fat binary did make it so their app can be built for both simulators and devices while testing, but once again, when they go to submit their app to the App Store they’ll be met with rejection. Turns out your app is only allowed to have device architectures to be considered a valid submission!
The only fix for this problem is to once again use the lipo command to strip out any unnecessary symbols before submitting an app to the App Store. Unfortunately, the app in question is out of your hands, so you’ll need to give your client a script to include in their build phases, or at least give them a heads up that they’ll need to do this before submitting.
Here's the one we use:
Don't worry, I'm quite sure we didn't write it. Could have come from this blog post; we may never know.
I agree that this feels overly clunky, but sometimes there’s not much you can do about it.