How to correctly configure building private OS X frameworks
July 09, 2014
I’ve been recently dealing with building a private / embeddable framework on OS X. By “private framework”, I mean a framework that’s not installed system-wide, but one that’s bundled with an OS X application. Now that frameworks are also coming to iOS, it’s probably of wider interest, although this specific post is only about OS X. I spent several hours yesterday trying to get it to work, and thought to just document it, so that I wouldn’t forget myself.
TL;DR version: you need to change some build settings to the following values:
Skip Install = YES Installation Directory = @executable_path/../Frameworks
You also need to add a “Copy files” build phase to your application target to copy the framework into its “Frameworks” group. And you also need to think about code signing, which I’m not covering here.
Okay. That was the summary. Below is an exploration of my adventures. And grab the example project.
The test project
I created a test project with Xcode 5.1.1, consisting of the default Cocoa application target. I then added a Cocoa Framework target with default settings. My test app is called HelloFramework, and the framework is simply called Hello. The code for the framework is very simple, here’s the header.
@interface Hello : NSObject + (void)helloFromClass; + (instancetype)sharedHello; - (void)helloFromInstance; @end
And the implementations imports the framework:
Okay, so far, so good. We add the Hello framework to our app target in “Link Binary with Libraries” phase. We add a new “Copy Files” phase to bundle the framework into the application. We run it in Xcode, and everything looks good. Dialog boxes come up.
Now, things start to get a bit weird.
Let’s archive the app target. The first sign of trouble is that the archive is not of type “Mac App Archive” as expected for the Mac app, but is of type “Generic Xcode Archive”, which most of the time in Xcode means “Probably Not What You Want”. But okay. Let’s try distributing it. I only have the choice of “Save Built Products” instead of exporting the app for distribution. Let’s do this anyway.
Here’s what’s exported.
So there’s both the application and the framework. But we don’t need the framework; we just want the application. If we peek inside the app bundle, we even see that is has a correct Frameworks group with the Hello.framework nicely sitting in there.
Well, let’s try to run the app. BOOM, it crashes.
What the what? Well, if we look at the error, it says right there what the problem is.
Dyld Error Message: Library not loaded: /Library/Frameworks/Hello.framework/Versions/A/Hello Referenced from: /Users/USER/Desktop/*/HelloFramework.app/Contents/MacOS/HelloFramework Reason: image not found
Okay, so it’s trying to load the library from a global path. Why is that?
System vs private frameworks
There’s two kinds of frameworks. The system ones go in /Library/Frameworks, and can be used by multiple applications. The default Cocoa Framework template seems to be set up for building these. I’m not sure what their future is, though, since a lot of app distribution happens through Mac App Store and these apps are sandboxed and I don’t think they can install system frameworks.
The other kind of frameworks, private frameworks, are the ones that are embedded in the actual app bundles. These are what we’re trying to build, and are failing so far in the example.
Turns out that the install location of a framework is part of its build settings. When we look at the Installation Directory of our framework target, it is by default, unsurprisingly, set to
$(LOCAL_LIBRARY_DIR)/Frameworks, which resolves to
/Library/Frameworks on my system. This is not what we want. We want the framework to be embeddable in apps, and so the installation path should be relative to the app that contains the framework.
What’s the right value? I found this post to be useful when working through this topic, but they recommend changing a bunch of values that don’t seem to be necessary. I found the official doc which says, hidden inside all the other wisdom, that the Installation Directory for private frameworks should be
@executable_path/../Frameworks, which looks pretty reasonable.
Great! Let’s change the installation path. When we now archive our app, we see that it creates “Mac App Archive” as expected, we can run it, and it no longer crashes. Fantastic.
There’s a warning when archiving the app target, though. Let’s get rid of that.
Fixing the last warning
The warning says:
Warning: Installation Directory starts with '@executable_path' but Skip Install is disabled.
It’s not super clear, but it’s clear enough if you’ve dealt with iOS app archiving troubles before and are familiar with Skip Install build setting. Basically, for a given target, Skip Install says (in the negative) whether it’s an installable target or not, which roughly translates to “Is this the app that the user will run” (in my mind, anyway). Why it needs to do that in the negative, I don’t know, calling it “Installable” would be much clearer. But just negate it in your head and it makes sense.
Skip Install = NO makes sense for system frameworks. You do want them to be installable. Although how that actually works beyond them being exported when archiving, I don’t know exactly. Anyway, in our case, for our private framework, we don’t need it to be installable so we set it to YES, and boom, the warning goes away.
The one thing to note is that there’s no nice way to archive the framework now, since it’s not an installable target. If we want to use it in another app as a pre-built thing, I just build it in Xcode and then grab the build result from the build products folder on the disk, probably with Release configuration if it’s meant to be used by others.
The mysteries remain
A few more things to note.
How is a normal person supposed to find and gather this info? I spent a few hours googling and trying out random stuff, as I wasn’t too familiar with frameworks, before I converged on this info. I think it could be a lot cleaner.
Why did the framework with wrong installation settings work when I was running the app in Xcode, and crash after exporting and trying to run the same app outside Xcode? I guess Xcode tries to be helpful here, when the framework project is part of your app workspace, and somehow masks out all these installation problems. But I think it was harmful in this case, because it really obscures the problem and shows there’s no substitute for testing the exported end product. Some of Xcode’s ways remain mysterious to me.
How does all this interact with code signing, and how will this work with Xcode 6 and iOS 8 where frameworks are now also supported? I don’t know enough about these topics right now to cover them myself, but maybe I’ll do a follow-up once I drill down into that.
Anyway, you can grab my example project here.