Understanding dyld @executable_path, @loader_path and @rpath
If you have added any third party dynamic framework to an iOS app, you might have run into a cryptic error that reads something like:
What is this
@rpath thing in the error message?
@rpath stands for Runpath search path. To understand what it means and why we need it, we need to take a step back and look at how dynamic libraries (called dylibs in macOS and iOS world) link with other dylibs and executables. We also need to understand the meaning of
@loader_path before looking into
@rpath This is best demonstrated by examples.
NOTE : I am using C for example code, but the concept stays the same for both Objective C and Swift. Reason for choosing C is easier compilation from command line.
What is @executable_path?
Starting in a fresh empty directory -
~/tests/blog/ in my case - let’s create two test files -
Cat.c into a dylib and
main.c into an executable.
-L stands for library search path.
-l specifies the name of dylib to link against, without the
lib prefix and
-o specifies the name of final output file.
Let’s go ahead and run the
We get the cat sound, as expected. Let’s inspect how the dylib has been linked to our executable. We can use
otool for this task. The
-L option prints paths to all dynamic libraries used by an executable. Let’s call these paths install paths.
The first install path
libCat.dylib is a relative path. This means our
main executable expects to find
libCat.dylib in same directory as it is executed from. If we try to run
main from any other directory, we get an error.
We can change the relative install path
main using a utility called
install_name_tool1. But what should we change it to? We could change it to absolute path, and that would work on our system, but if you distribute
libCat.dylib to someone else, it will likely fail on their system.
Solution here is to use a special variable called
@executable_path. When dyld encounters this variable at link time, it gets resolved to path of directory containing the executable. In my case since
main is in
@executable_path resolves to
~/test/blog/. Let’s fix the install path using
~/tests/ directory again.
What is @loader_path?
@loader_path, we need to add some complexity to our test case. Let’s create another dylib in a different directory, and make this dylib depend on our Cat dylib.
Animal/Animal.c into a dylib. We will need to link it to
main.c to make generic animal sounds rather than cat sounds, and recompile it. We will need to link against
libAnimal rather than
Just like before, we know that we can execute
~/tests/blog/, but not from
~/tests or any other directory. We also know the fix for this. So let’s go ahead and do that.
~/tests/ fails this time though.
The error tells us that
libAnimal could not find
libCat. Let’s check the install paths for
We need to fix the relative install path to
libCat.dylib here. But what should we change it to? Using
@executable_path would work - but for our
main executable only. Dylibs are meant to shared across muliple clients2, all of which can be in different paths. This means
@executable_path will resolve to different values depending on the executable being run.
Let’s take a step back and look at the dependency tree we have.
main depends on
libAnimal depends on
libCat doesn’t depend on anything.3
@executable_path will always resolve to path of
main. No matter if it’s
main doing the loading of
libAnimal doing the loading of
libCat. dyld provides another variable -
@loader_path - that resolves to path of client doing the loading. In the above tree there are two loads happening. Let’s write down the values of the two variables for both loads.
|main -> libAnimal||~/tests/blog/||~/test/blog/|
|libAnimal -> libCat||~/tests/blog/||~/test/blog/Animal/|
Knowing this, we can now change the install path in Animal dylib from
main will now succeed from any directory. In fact, if you were to add a new executable
foo/main that depends on
libAnimal, you would only need to set the install path for
foo/main itself4. No change is needed to either
libAnimal as long as relative path between them remains the same, but that is unavoidable. They are now “shared libraries” in true sense of the word.
NOTE : For executables,
@executable_pathmean the same thing.
Short note on Install ID
Before moving on to
@rpath, let’s understand a small concept called install IDs. Check the
otool -L output for any dylib
For dylibs, the first entry is not an install path, but rather an install ID. When another client links to this dylib, it is the dylib’s install ID that gets copied as install path in the client.
What is @rpath?
In a large project with muliple clients in different locations depending on each other, having to keep track of
@loader_path can get messy quickly. In such cases, we can use
@rpath. Unlike the two variables we have seen till now,
@rpath doesn’t have any special meaning to dyld. It is upto us to define a value (or values) for
@rpath for each client.
@rpath adds a level of indirection that can simplify things.
Let’s modify our test case to make use of
@rpath. Add another executable
foo/main.c with same source as
main.c. We won’t compile it yet. Our directory structure looks like this :
First step is to pick a path as an anchor path in our directory structure. Let’s pick
~/tests/blog/ as our anchor.
Next, let’s change the dylib install IDs to
@rpath/zzz where zzz is relative path from anchor to the dylib.
libCat.dylib this works out to
Animal/libAnimal.dylib this works out to
Next, let’s add an
@rpath to our executables with value equal to
@loader_path/zzz where zzz is relative path from executable to our anchor.
foo/main.c this works out to
main.c this works out to
@loader_path. We can use
install_name_tool to add
@rpath to the compiled executable, but since the install IDs for
libCat have changed after
main.c was compiled, it’s best to re-compile and re-link it so that it picks up the updated IDs.
After these changes, we can move our
~/test/blog/ directory anywhere, even a different system, and the two executables will continue to run - as long as the directory structure within
~/tests/blog/ itself doesn’t change.
Note that you can define more than one
@rpath value for an executable, either at link time or later using
install_name_tool. dyld will try all values in order to check for existence of dylibs.
Back to the error message
Knowing all this, we are now in a far better position to understand what the initial error message means, and how to go about fixing it. Let’s analyze the error -
The executable is
<long_path_name>/TestApp.app/TestApp. Dylib is
TestKit. The executable could not find the dylib at
For iOS apps, all third party frameworks reside inside a
Frameworks directory inside app directory. So the actual path to dylib is
<long_path_name>/TestApp.app/Frameworks/TestKit.framework/TestKit. The anchor directory is
<long_path_name>/TestApp.app/Framewoks/. So to figure out the reason for this error, we can check two things -
@executable_path/Frameworks/works too since both mean the same thing for executables. If you have the source code, you can check this in target’s build settings (
LD_RUNPATH_SEARCH_PATHS). If not,
otool -lis your friend.
TestKitdylib has install ID of
@rpath/TestKitFramework.framework/TestKit. If you have the source, check this in build settings (
LD_DYLIB_INSTALL_NAME). If not,
otool -lis your friend again.
TestKit.frameworkis actually present inside
Frameworksdirectory. You need to embed the framework inside the app for this.
When you add a new framework to an app, Xcode takes care of all this settings for you. But it’s good to know what’s happening behind the scenes :)
man dyld offers a lot more insight into the things glossed over in this post.
Check out the manpage for
install_name_toolto see what all it can do. It’s a short read. ↩︎
Client can mean either an executable or a dylib. ↩︎
libSystemwhich is located at
/usr/lib/libSystem.dylib. We can ignore this for our purpose. ↩︎
For any client, having correct install paths to all dylibs it depends on is unavoidable, unless the dylib is present at a standard location like