Skip to content

App hangs in dyld at launch: injected PackageFrameworks rpath shadows the app bundle's embedded framework #728

Description

@jamminalley

Environment: Skip 1.9.7, Xcode 26.3, Swift 6.3.0 (swift-6.3-RELEASE), macOS 26.5.2 (arm64), iOS 26.3 simulator. Skip Fuse.

Summary

A stock skip create app (Skip Fuse) installs and launches on the iOS simulator but hangs
on a blank launch screen and never reaches main. No crash, no logs, no error.

The cause is dynamic-library resolution order. The linked app binary carries an rpath
pointing into the build tree:

$ otool -l CarpoolApp.app/CarpoolApp | grep -A2 LC_RPATH
    path /usr/lib/swift
    path <PROJECT>/.build/Darwin/DerivedData/Build/Products/Debug-iphonesimulator/PackageFrameworks
    path @executable_path/Frameworks

The PackageFrameworks entry is ordered ahead of @executable_path/Frameworks, so dyld
loads CarpoolApp.framework from the build tree rather than from the app's own bundle — even
though the framework is correctly embedded in the bundle by the Embed Frameworks phase.

DYLD_PRINT_SEARCHING=1 confirms it:

find path "@rpath/CarpoolApp.framework/CarpoolApp"
  ...
  found: dylib-from-disk: "<PROJECT>/.build/.../PackageFrameworks/CarpoolApp.framework/CarpoolApp"

...and then output stops. A sample of the hung process shows 100% of samples parked in
dyld4::JustInTimeLoader::loadDependentsSyscallDelegate::mapFileReadOnly
dyld3::openopen_with_subsystem. The process is blocked in a file open, not spinning.

Why this matters even though it usually "works"

Loading the framework from the build tree normally succeeds, so this is invisible most of the
time. But it makes a launched app depend on its build directory remaining readable at runtime,
which is not a property an installed app should have. When that open blocks for any reason, the
app hangs before main with no diagnostic whatsoever — the hardest possible failure to debug.

In our case the trigger is location: the project lives under ~/Documents, a macOS
privacy-protected directory. We confirmed this by experiment:

Project location Injected rpath present Result
~/Documents/Development/... yes hangs at blank launch screen
~/Development/... (same scaffold, unmodified) yes launches normally
~/Documents/Development/..., rpath stripped post-link no launches normally

We did not prove the macOS mechanism behind the blocking open (TCC is the strong suspect;
tccd logged nothing and TCC.db was unreadable). Ruled out: iCloud Drive sync (not enabled),
endpoint-security extensions (none installed), code-signature differences between the two
framework copies (both ad-hoc, identical), architecture mismatch, and stale DerivedData.

Reproduction

  1. skip create a Fuse app in a project directory under ~/Documents.
  2. skip app launch.
  3. iOS installs and launches to a blank white screen and stays there. Android is unaffected.
  4. otool -l the app binary and observe the PackageFrameworks rpath ahead of
    @executable_path/Frameworks.

Where the rpath comes from

Not from the Skip scaffold's own settings. Darwin/CarpoolApp.xcconfig and the pbxproj set
only:

LD_RUNPATH_SEARCH_PATHS = @executable_path/Frameworks

and xcodebuild -showBuildSettings confirms that is the effective value. Xcode's SwiftPM
integration injects -rpath $BUILT_PRODUCTS_DIR/PackageFrameworks at link time for the
type: .dynamic library product declared in Package.swift, outside of any visible build
setting.

So this may be Xcode behaviour rather than a Skip bug per se — but the scaffold's
.library(name:, type: .dynamic, ...) product is what triggers it, and the resulting failure
mode lands squarely on Skip users. Worth either documenting or defending against in the
scaffold.

Workaround we're using

A post-link build phase in the app target, after Embed Frameworks and before Xcode's CodeSign
step:

RPATH="${BUILT_PRODUCTS_DIR}/PackageFrameworks"
APP="${TARGET_BUILD_DIR}/${EXECUTABLE_FOLDER_PATH}"
for BIN in "${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}" "${APP}/${PRODUCT_NAME}.debug.dylib"; do
  [ -f "${BIN}" ] || continue
  if otool -l "${BIN}" | grep -q "path ${RPATH} (offset"; then
    install_name_tool -delete_rpath "${RPATH}" "${BIN}"
  fi
done

With the rpath removed the app resolves only its embedded framework and launches correctly,
with the build tree left entirely in place.

Question for maintainers

Is the PackageFrameworks rpath needed at runtime for Skip Fuse apps, or is embedding the
frameworks sufficient? If the latter, stripping it in the scaffold would make Skip apps
independent of their build directory and remove a very hard-to-diagnose class of launch hang.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions