skiptools / skiptools/skip

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

Open
#728 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Swift
Stars
3.2k
Forks
106
Avg merge
6d 13h
Merged PRs (30d)
1

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Package.swift, Darwin/CarpoolApp.xcconfig, and the generated pbxproj, then reproduce with skip create and inspect the app binary using otool -l. Trace how the dynamic product creates the PackageFrameworks rpath and check whether the embedded framework is sufficient at runtime. Done means the behavior is resolved or documented with a verified launch test for projects under ~/Documents.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
build-system, mobile
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.