rea-reverse

Fix "dyld: Library not loaded: @rpath" on macOS

Written for REA 6.8.0Published by rea-reverseUpdated 3 min read
On this page

Library not loaded: @rpath/libfoo.dylib means dyld substituted every LC_RPATH entry of the loading image into @rpath, and none of the resulting paths held the file. The fix is either to put the dylib at one of the paths dyld printed under tried:, or to add an rpath (install_name_tool -add_rpath) that points to where the dylib really is.

Everything below was run on macOS 27.0.1 (Apple silicon) with Apple clang 21.0.0, on a two-file program built for this article.

Trace macOS dylib resolution with REASilent walkthrough with English captions.

Reproduce the error

A library whose install name is @rpath/libanswer.dylib, and an executable that searches @executable_path/lib:

mkdir -p sample/lib missing
printf 'int answer(void) { return 42; }\n' > sample/lib/answer.c
printf 'extern int answer(void); int main(void) { return answer() == 42 ? 0 : 1; }\n' > sample/main.c
clang -dynamiclib sample/lib/answer.c \
  -Wl,-install_name,@rpath/libanswer.dylib -o sample/lib/libanswer.dylib
clang sample/main.c -Lsample/lib -lanswer \
  -Wl,-rpath,@executable_path/lib -o sample/demo
cp sample/demo missing/demo      # same bytes, no lib/ next to it

./sample/demo exits 0. The copy without its library aborts before main runs:

$ ./missing/demo
dyld[7720]: Library not loaded: @rpath/libanswer.dylib
  Referenced from: <E4183D4B-01D7-36A3-A542-CA09BC4F425A> /…/work/missing/demo
  Reason: tried: '/…/work/missing/lib/libanswer.dylib' (no such file), '/…/work/missing/lib/libanswer.dylib' (no such file)
Abort trap: 6

Exit status was 134 (SIGABRT). The tried: list is the most useful line: it is every rpath already expanded. Here the same path appears twice; we only had one LC_RPATH, so a duplicate is not a sign of two search entries. If tried: lists a path you expected to work, check the spelling and architecture of the file there; if your intended folder is not in the list at all, the rpath is missing.

Read the names and rpaths with otool

$ otool -L sample/demo
sample/demo:
	@rpath/libanswer.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1359.0.0)

$ otool -l sample/demo | grep -A2 LC_RPATH
          cmd LC_RPATH
      cmdsize 40
         path @executable_path/lib (offset 12)

$ otool -D sample/lib/libanswer.dylib
sample/lib/libanswer.dylib:
@rpath/libanswer.dylib

otool -L gives the name the executable asks for, otool -l gives its search paths, and otool -D gives the install name the dylib itself carries (it is copied into the executable at link time). The three tokens resolve like this:

  • @executable_path is the directory of the main executable of the process, even when a plugin or framework asks for the library.
  • @loader_path is the directory of the image that contains the load command; use it inside frameworks and plugins.
  • @rpath is replaced by each LC_RPATH of the loading image and of the images that loaded it, in order.

/usr/lib/libSystem.B.dylib has no file on disk on current macOS; it comes from the dyld shared cache, so “file not found” with ls there is normal.

Three fixes, all tested

Ship the dylib where the rpath points. mkdir -p missing/lib && cp sample/lib/libanswer.dylib missing/lib/ made ./missing/demo exit 0. For an app bundle that usually means copying it into Contents/Frameworks in a build phase.

Add an rpath for where the dylib really is. With the library in missing/Frameworks/ instead:

$ install_name_tool -add_rpath @executable_path/Frameworks missing/demo
$ otool -l missing/demo | grep -A2 LC_RPATH
          cmd LC_RPATH
      cmdsize 40
         path @executable_path/lib (offset 12)
--
          cmd LC_RPATH
      cmdsize 40
         path @executable_path/Frameworks (offset 12)
$ ./missing/demo; echo $?
0

In an Xcode or CMake project, set it at link time instead (LD_RUNPATH_SEARCH_PATHS, -Wl,-rpath,…, or CMAKE_INSTALL_RPATH) so the next build does not undo the fix.

Rewrite the dependency itself. install_name_tool -change @rpath/libanswer.dylib @executable_path/sample/lib/libanswer.dylib fixed3 changed the otool -L line to the new path and the program exited 0. To change the name a dylib advertises for future links, use install_name_tool -id on the dylib.

After editing, codesign -dv on our binary still reported Signature=adhoc and it ran. A Developer ID signed or notarized app is different: edit before signing, or re-sign (codesign --force --sign …) afterwards, and with library validation enabled every loaded dylib must be signed by the same team or by Apple.

When the static view is not enough

DYLD_PRINT_RPATHS=1 and DYLD_PRINT_LIBRARIES=1 show what dyld does at run time, but SIP strips DYLD_* variables for protected binaries, and hardened-runtime apps ignore them unless entitled.

For a large bundle with many nested frameworks, reading every otool -l by hand gets slow. REA, a third-party open-source tool, has a trace-dylib-resolution command that builds the whole graph statically without launching anything. On the sample above with REA 6.8.0 (which needs Node 22.23.4+, 24.22.1+ or 26.12.1+), rea trace-dylib-resolution sample/demo --format json reported the @rpath/libanswer.dylib edge as resolved through candidate lib/libanswer.dylib, and for missing/demo the candidate was absent with the finding required-load-unresolved. /usr/lib/libSystem.B.dylib showed as outside-target / undetermined, which means “not evaluated”, not “missing”. It does not check code signing or compatibility versions, so a resolved edge still is not a successful launch.

To find the executable inside an .app first, see macOS app structure; if the error mentions the wrong architecture rather than a missing file, check the slices with the universal binary guide.