This post has been sitting around waiting for my NDA to lift as Leopard is released into the wild. This post is about migrating from executable_path to rpath as a means of bundling relocatable libraries with your application. This is a technique that I used to make Maya relocatable.
The Problem
Sometimes developers find it is necessary to pack up a few extra dylib shared libraries inside an application bundle. In that case, it becomes necessary to do some fancy linking so that your application is relocatable.
Normally a Mach-O binary (eg a dylib, bundle, or executable) maintains a list of shared libraries that it links to. By default that list will contain a list of absolute paths to the libraries. Lets look at a random example using otool -L
to dump the list of linked libraries.
# otool -L /usr/bin/more
/usr/bin/more:
/usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0,...
/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, cur...
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, cu...
You can see that the executable "more" links against three system dylibs. The absolute path to each dylib is listed. If you moved one of those dylibs, more
would cease to function.
The system libraries are going to stay put. The real problem occurs when you use a non-system library (such as an open source library) in your application, and you'd like to stick it in your application bundle under .../Contents/Libraries
. Lets say my example application is CodeShorts.app
and I'm using the library liblua.dylib
to use to embed the Lua scripting language (note I'd use Python, but it comes with OS X and therefore makes a terrible example). I'd like to have the following application layout.
CodeShorts.app/
Contents/
MacOS/
CodeShorts
Libraries/
liblua.dylib
Now, you could link to liblua.dylib using the path /Applications/CodeShorts.app/Contents/Libraries/liblua.dylib
, and hope that nobody moves your app. However, many Mac users love to spend hours a day reorganizing their beloved Macs, and who knows where CodeShorts.app will end up (hopefully not the trash).
The Old Way: executable_path
Apple has a couple of solutions for this. Until recently (pre Leopard), this solution was to stick the special name @executable_path
into the library path as referenced by your application. So, CodeShorts
would look for @executable_path/../Libraries/liblua.dylib
. At runtime dyld
(the dynamic linker) would substitute @executable_path
for /Applications/CodeShorts.app/Contents/MacOS
and you'd be off to the races. You could move CodeShorts.app around all afternoon and it would still run (unless you moved it to the Trash). This works reasonably well, but it does impose some restrictions. The two main restrictions are as follows: 1. the relative path between the executable and the library is fixed. If you want to use the same shared library from two executables, then both executables must share the same relative path to the dylib. 2. you can only specify one place to look for the shared library. You cannot have a multi-location search path
The New Way: rpath
If you are a developer who has used this technique, then you're in for a surprise when you try to use the old -executable_path
flag on ld
under Leopard. It's gone. Leaving you to wonder "what do I do now??"
One of the lovable, but frustrating things about Apple is that they force forward progress on their users and developers.
Now that Leopard is the prime cat, the new mechanism for this is rpath
which will be familiar at least in name to developers from other platforms such as Linux. Apple has, of course, thrown in a few extras as they are wont to do. The main resources for the rpath
changes are the dyld Release Notes for Mac OS X v10.5 and the ld manpage on Leopard.
At its most basic, the -rpath
flag to ld
allows you to specify a list of directories where dyld
should look for library dependencies. So, for example if you link CodeShorts
to liblua.dylib, you could specify -rpath /Applications/CodeShorts.app/Contents/Libraries -rpath /opt/local/lib.
That would make the CodeShorts
application look in two places for liblua.dylib
and any other dependencies. That's a good start, but it does not address the relocation problem we set out to solve, unless you can guess every place the user might stick your application (-rpath /Users/JoeUser/.Trash
).
To solve this problem, Apple has introduced two new special funky @
names that you can use. One is for use in your library's name that is embedded in the library. And one is for use in your rpath
. We'll get to those in a minute.
First we need to look a bit deeper into library dependencies on OS X. When you link a shared library, framework, or bundle on OS X, its path is embedded into the Mach-O binary. That name is later used by other Mach-O binaries that reference your library. So, lets go back to our example above where we looked at /usr/bin/more
.
# otool -L /usr/bin/more
/usr/bin/more:
/usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0,...
/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, cur...
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, cu...
You can see that more
uses /usr/lib/libncurses.5.4.dylib
. Now, where did that path come from? When more
was linked, the linker looked at libncurses.5.4.dylib
and read the path name for the library and then added it to the list of dependencies for more
. We can look at /usr/lib/libncurses.5.4.dylib
and see what the path name is:
# otool -L /usr/lib/libncurses.5.4.dylib
/usr/lib/libncurses.5.4.dylib:
/usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0,...
/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, cur...
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, cu...
The first entry is the name of the Mach-O binary. When more
was linked, ld
looked at libncurses.5.4.dylib
and read the path /usr/lib/libncurses.5.4.dylib
and added it to more
's dependencies.
Now, back to those two special @
names. The first special name is @rpath
. This name can be used to lookup a library at a path relative to the directories in the loader's rpath. You would use this when the library will live in a certain subdirectory somewhere along the rpath. While it might not be obvious why you would need this, it becomes quickly apparent when you think of Frameworks where the actual Mach-O library lives buried in a sub directory. You don't want to have to specify an rpath entry for each individual framework. You just want to add your Framework directory to the rpath and have each framework picked up via its own relative install name.
It's time to create a contrived example. So, lets say you have two frameworks: BigLibrary.framework
and SmallLibrary.framework
. Now, lets say that they both live in /opt/Frameworks
(of course, you would usually put them in /Library/Frameworks
, but OS X knows about that directory, so its not a good enough contrived example). Your two Mach-O binaries would be as follows:
/opt/
Frameworks/
SmallLibrary.framework/
Versions/
1.0/
SmallLibrary
BigLibrary.framework/
Versions/
1.0/
BigLibrary
When you link to those Frameworks from your application, you would use the flag -rpath /opt/Frameworks
. That way, your application will search /opt/Frameworks
when it loads. However, there's no Mach-O dylibs in that directory. But, that's ok. Because, when you linked SmallLibrary
you cleverly specified -install_name @rpath/SmallLibrary.framework/Versions/1.0/SmallLibrary
. Likewise, you specified the relative path for BigLibrary as well. That means that dyld
will look in /opt/Frameworks
for SmallLibrary.framework/Versions/1.0/SmallLibrary
and, it will find it. So, that takes care of @rpath
.
Now, this still doesn't solve the problem of relocatable libraries (get on with it will you!).
This brings us to the second special name, @loader_path
. This special name may be used at the beginning of a path that is passed to the -rpath
flag. At runtime, dyld
replaces @loader_path
with the path to the Mach-O binary that loaded the library. The nice thing here is that the loader can be an executable or another shared library of some sort. This is much more flexible than the old @executable_path
solution, which only referred to the main executable.
Now, we have the tools to do what we really want to do. We can now hide libraries and framework bundles inside our application bundle and have them found no matter where the user drags the application.
Lets say that we want the following disk structure for our application CodeShorts, using all of the frameworks and libraries that we described above.
CodeShorts.app/
Contents/
MacOS/
CodeShorts
Libraries/
liblua.dylib
Frameworks/
SmallLibrary.framework/
Versions/
1.0/
SmallLibrary
BigLibrary.framework/
Versions/
1.0/
BigLibrary
We've got two frameworks and a library embedded in our application. We need to tell the application CodeShorts
what directories to look in when searching for the libraries and frameworks, and we've got to give the libraries and frameworks the correct relative paths so that they'll be found.
When we link the application CodeShorts
we are going to give it two places to look for libraries and frameworks. We'll do that using the -rpath
flag something like this:
Mach-O Binary:
CodeShorts.app/Contents/MacOS/CodeShorts
Linker Flags:
-rpath @loader_path/../Libraries -rpath @loader_path/../Frameworks
NOTE: gcc doesn't know the -rpath flag, so you've got to pass it through to ld like this: -Wl,-rpath,@loader_path/...
This means that, when dyld
loads the CodeShorts
application, it will look for libraries in the Libraries
and Frameworks
directories inside the application.
When the libraries are linked (obviously needs to happen before the app is linked) they should be told that their install name is relative to the rpath. For liblua.dylib this is easy.
Mach-O Binary:
CodeShorts.app/Contents/Libraries/liblua.dylib
Linker Flags:
-install_name @rpath/liblua.dylib
For the frameworks, this is a little trickier, but not much. You need to give the location of the actual dylib, which is burried in a sub directory of the framework. But, you need to give it relative to the rpath.
Mach-O Binary:
CodeShorts.app/Contents/Frameworks/SmallLibrary.framework/Versions/1.0/SmallLibrary
Linker Flags:
-install_name @rpath/SmallLibrary.framework/Versions/1.0/SmallLibrary
Mach-O Binary:
CodeShorts.app/Contents/Frameworks/BigLibrary.framework/Versions/1.0/BigLibrary
Linker Flags:
-install_name @rpath/BigLibrary.framework/Versions/1.0/BigLibrary
That should be it. This is probably a long winded explanation of a simple concept. However maybe my verbosity balances out the terseness of the Apple documentation.