Using Frameworks Within NSBundles

Or more specifically: “Using frameworks that use other frameworks within NSBundles”. 

For a project that I’ve been quietly working on for some time now, I’ve been using the Connection Kit framework. Initially I had issues getting my NSBundle to load the framework properly.

I’ve used frameworks within NSBundles in previous projects, like Code Complete. As the framework I used there did not itself require a framework, and I had its source, I could simply change the install name to @loader_path/../Frameworks in Xcode.

Use @loader_path as opposed to @executable_path

Use @loader_path as opposed to @executable_path

I wanted to use Connection Kit for my current project, but ran into issues as it requires a few other frameworks to function. Many months ago Stack Overflow helped me work around this by suggesting I copy the internal framework’s source files directly into Connection Kit.

Everything went well until I updated Connection Kit. Suddenly it required more internal frameworks, and copying their source became impractical.

Into the breach

During my previous attempts to make Connection Kit work with my bundle I came across this post: Embedding frameworks in loadable bundles, but wasn’t patient enough to make it work.

This time I obviously had no other choice, so put some time into reading the man pages for both install_name_tool & otool. The former allows one to change dynamic shared library install names, the latter to view them (among other things).

Ultimately, the solution was to change the Connection Kit framework’s name and to change the internal names of each framework it required with:

install_name_tool -id executable
install_name_tool -change old_name new_name executable

Just give me the details

The exact commands I used were:

install_name_tool -id \
"@loader_path/../Frameworks/Connection.framework/Versions/A/Connection" \
"$SRCROOT/Frameworks/Connection.framework/Versions/A/Connection"

This change was required to allow my bundle to load the framework correctly.

To ensure Connection Kit could load its internal frameworks, I also needed to perform:

# Update internal framework names
install_name_tool -change "@executable_path/../Frameworks/CURLHandle.framework/Versions/A/CURLHandle" \
"@loader_path/../../../CURLHandle.framework/Versions/A/CURLHandle" \
$CONNECTION
 
install_name_tool -change "@executable_path/../Frameworks/DAVKit.framework/Versions/A/DAVKit" \
"@loader_path/../../../DAVKit.framework/Versions/A/DAVKit" \
$CONNECTION
 
otool -L $CONNECTION
 
# Fix DAVKit name
install_name_tool -id \
"@loader_path/../Frameworks/DAVKit.framework/Versions/A/DAVKit" \
"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/DAVKit.framework/Versions/A/DAVKit"
 
otool -D "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/DAVKit.framework/Versions/A/DAVKit"
 
# Fix CURLHandle name
install_name_tool -id \
"@loader_path/../Frameworks/CURLHandle.framework/Versions/A/CURLHandle" \
"$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/CURLHandle.framework/Versions/A/CURLHandle"
 
otool -D "$TARGET_BUILD_DIR/$FRAMEWORKS_FOLDER_PATH/CURLHandle.framework/Versions/A/CURLHandle"

This updated Connection Kit’s internal names for the two frameworks, allowing it to load them successfully. I also updated the internal frameworks’ names here.

While I was working this out, I made heavy use of otool, which allows one to see the name & install paths for a given binary, for example, before performing the above on Connection Kit & internal frameworks (irrelevant lines removed):

otool -D Connection.framework/Versions/A/Connection 
Connection.framework/Versions/A/Connection:
@executable_path/../Frameworks/Connection.framework/Versions/A/Connection
 
otool -L Connection.framework/Versions/A/Connection
Connection.framework/Versions/A/Connection:
	@executable_path/../Frameworks/Connection.framework/Versions/A/Connection (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/../Frameworks/CURLHandle.framework/Versions/A/CURLHandle (compatibility version 1.0.0, current version 1.0.0)
	@executable_path/../Frameworks/DAVKit.framework/Versions/A/DAVKit (compatibility version 1.0.0, current version 1.0.0)

And after:

otool -D Connection.framework/Versions/A/Connection
Connection.framework/Versions/A/Connection:
@loader_path/../../Contents/Frameworks/Connection.framework/Versions/A/Connection
 
otool -L Connection.framework/Versions/A/Connection
Connection.framework/Versions/A/Connection:
	@loader_path/../../Contents/Frameworks/Connection.framework/Versions/A/Connection (compatibility version 1.0.0, current version 1.0.0)
	@loader_path/../../../CURLHandle.framework/Versions/A/CURLHandle (compatibility version 1.0.0, current version 1.0.0)
	@loader_path/../../../DAVKit.framework/Versions/A/DAVKit (compatibility version 1.0.0, current version 1.0.0)

I put all of this into a “Run Script” build phase -

Xcode Loader Path Change

Xcode run script build phase

After compiling & running the parent application, my bundle loaded perfectly.

I learned a lot about OS X development during this process. Have you had to do anything similar to convince your projects to run?

Comments (5) | Trackback