So last week I spent the better part of the day trying to figure out what exactly was going when I was trying to build a component using SourceKittenFramework.
It turns out that not all frameworks are created equal in Xcode. Honestly, this wouldn’t be such a big deal if Swift properly supported static libraries, as the rabbit hole for this problem is rooted in a bunch of hacks to make command-line tools work properly with Swift that have dependencies.
I’ll provide a little story about my experience this week.
Xcode supports the concept of embedding frameworks into your bundle. This is essentially the same thing as the old “Copy Files” build phase where you can copy a dependency into your app bundle under a particular directly, such as “Frameworks”.
However, there is an extremely important distinction between the “Copy Files” build phase and the “Embed Frameworks” option.
The normal output of a framework looks like this:
├── Headers -> Versions/Current/Headers ├── Modules -> Versions/Current/Modules ├── MyFramework -> Versions/Current/MyFramework ├── Resources -> Versions/Current/Resources └── Versions ├── A │ ├── Headers │ │ ├── MyFramework-Swift.h │ │ └── MyFramework.h │ ├── Modules │ │ ├── MyFramework.swiftmodule │ │ │ ├── x86_64.swiftdoc │ │ │ └── x86_64.swiftmodule │ │ └── module.modulemap │ ├── MyFramework │ └── Resources │ └── Info.plist └── Current -> A
This provides all of the necessary content to be able to use this framework both at runtime and as a developer-friendly framework; it has the headers and the module definitions necessary when building and linking against the library.
However, the frameworks that get embedded strip out all of that information and you end up with something like this:
├── MyFramework -> Versions/Current/MyFramework ├── Resources -> Versions/Current/Resources └── Versions ├── A │ ├── MyFramework │ ├── Resources │ │ └── Info.plist │ └── _CodeSignature │ └── CodeResources └── Current -> A
This contains only the content required to be used at runtime. Now, it makes sense why Xcode would do this, after all, it’s being packaged up for use within a target so it’s already built. Also, this can help reduce the size of bundles by removing all of the information that simply isn’t necessary to work at runtime.
Had I only known this before…
Now… the problem with all this of course is that when we start doing hacks to make thing work in the ever changing landscape of Swift and Xcode.
I ran into this when trying to use SourceKitten. As Swift doesn’t really have a good way to build testable command-line tools or static libraries, SourceKitten follows the pattern that a lot of other tools do: it builds an app target and then copies out the CLI tool and packages up its dependencies.
The start of the problems…
I don’t use Carthage or CocoaPods… but the reasons for that is outside of the scope for this. Needless to say, I simply clone the repo and ran
make install as the
ReadMe told me to do.
Everything is happily pulled down and everything is built properly. Then, SourceKitten is installed into my
/usr/local path. Great!
Specifically, it creates a structure like this:
sourcekitten is then set to
There is nothing wrong with this setup. It works great.
However… remember, my intention is to now take
SourceKittenFramework.framework, link it into my own app, and start converting my prototype that was shelling out to
sourcekitten to directly use the API.
So I do what seems reasonable:
- Create my app
- Create a
- Copy the
- Link the framework
And then I’m greeted with this:
<path/to/file:line:column> error: no such module 'SourceKittenFramework' import SourceKittenFramework ^
So I spend some time making sure my framework search paths are correct, inspecting the SourceKitten project, searching the web… no idea what is going on.
Ok… so I tried building from within Xcode and looking at the output of the
SourceKittenFramework target itself. I still don’t understand the problem yet.
I copy over the version of
SourceKittenFramework that is built from the target and I get this:
<path/to/file:line:col>: error: missing required modules: 'SWXMLHash', 'Yaml' import SourceKittenFramework ^
Ok… what is happening!? I still haven’t figured out all of the specialness of “embedded frameworks” at this point.
I tried mucking with the framework search paths to point to the
SWXMLHash frameworks that I clearly see are within the frameworks, but nothing seems to be working… Including copying the
SWXMLHash frameworks to be siblings of the
Ok… maybe source doesn’t work!? I download the framework from the releases page… SAME FLIPPING ERROR!
At this point, I’m quite frustrated, have no idea what is going on, and decide to call it for the day.
I come back the next day. OK, I’m going to get this to work!
I do, what I thought were mostly identical steps above.
Build success… ok, what the flying flip?
The key difference that I did this time was that I copied the
SourceKittenFramework.framework from the target output of the
At this point I was curious as to what had happened. This is where I started doing an analysis of what is different between each of the frameworks. See the “Embedded Frameworks” section above.
If you are providing frameworks to people that you expect to be able to develop with and not just use at runtime, please be sure to distribute the non-embedded framework version! Otherwise, well, all of your consumers will face the above issues.
SourceKitten tracking issue: https://github.com/jpsim/SourceKitten/issues/232
SourceKitten potential fix: https://github.com/jpsim/SourceKitten/pull/233