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:
- Build with ProjectA (dynamic framework) → TBD created in EagerLinkingTBDs
- Switch workspace to ProjectB (static framework) without cleaning DerivedData
- Build again → BUILD SUCCEEDED
- 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)
FRAMEWORK_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)" "$(inherited)" | Xcode still adds EagerLinkingTBDs first |
EAGER_LINKING = YES on static framework | Build failure - empty TBD |
OTHER_LDFLAGS modifications | Linker still uses TBD |
Expected Behavior
When a framework changes from dynamic to static, Xcode should remove the stale TBD file from EagerLinkingTBDs.
Suggested Fix
- Remove TBD files when building static library targets
- Track MACH_O_TYPE changes and invalidate TBD files accordingly