Stale TBD Files Cause Runtime Crash When Framework Changes from Dynamic to Static

Stale TBD Files Cause Runtime Crash When Framework Changes from Dynamic to Static

Summary

When using EAGER_LINKING=YES, Xcode generates TBD files for dynamic frameworks. When a framework changes from dynamic to static, Xcode doesn't remove the stale TBD, causing dyld: Library not loaded crash at runtime.

Environment

  • Xcode 16.4 (16F6), macOS Darwin 24.6.0, iOS Simulator 18.5

Steps to Reproduce

Project Structure:

  • MainApp (EAGER_LINKING=YES)
  • ProjectA/SharedLib (dynamic, mh_dylib)
  • ProjectB/SharedLib (static, staticlib)

Steps:

  1. Build with ProjectA (dynamic framework) → TBD created in EagerLinkingTBDs
  2. Switch workspace to ProjectB (static framework) without cleaning DerivedData
  3. Build again → BUILD SUCCEEDED
  4. Run app → CRASH: dyld: Library not loaded

Root Cause

Xcode adds -F EagerLinkingTBDs before -F Build/Products:

-F/.../EagerLinkingTBDs/Debug-iphonesimulator  ← checked FIRST
-F/.../Build/Products/Debug-iphonesimulator     ← checked SECOND

The linker finds the stale TBD first and treats the framework as dynamic, even though the actual binary is now static.

Evidence

# SharedLib is static
$ file SharedLib.framework/SharedLib
current ar archive random library

# But stale TBD still exists
$ ls EagerLinkingTBDs/.../SharedLib.framework/
SharedLib.tbd  ← STALE!

# MainApp incorrectly references dynamic library
$ otool -L MainApp.app/MainApp.debug.dylib | grep SharedLib
@rpath/SharedLib.framework/SharedLib  ← WRONG!

Attempted Workarounds (All Failed)

WorkaroundResult
FRAMEWORK_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)" "$(inherited)"Xcode still adds EagerLinkingTBDs first
EAGER_LINKING = YES on static frameworkBuild failure - empty TBD
OTHER_LDFLAGS modificationsLinker still uses TBD

Expected Behavior

When a framework changes from dynamic to static, Xcode should remove the stale TBD file from EagerLinkingTBDs.

Suggested Fix

  1. Remove TBD files when building static library targets
  2. Track MACH_O_TYPE changes and invalidate TBD files accordingly

Stale TBD Files Cause Runtime Crash When Framework Changes from Dynamic to Static
 
 
Q