Skip to content

feat(ios): embed dynamic SwiftPM products with embed - #6169

Draft
edusperoni wants to merge 1 commit into
feat/define-command-migrationfrom
feat/spm-dynamic-embed
Draft

edusperoni wants to merge 1 commit into
feat/define-command-migrationfrom
feat/spm-dynamic-embed

Conversation

@edusperoni

Copy link
Copy Markdown
Collaborator

Stacked on #6153 (feat/define-command-migration). Only the last commit belongs to this PR; the rest is the base. Retarget to main once #6153 merges.

PR Checklist

What is the current behavior?

Declare a Swift package product as dynamic and reference it from nativescript.config.ts:

.library(name: "MyProduct", type: .dynamic, targets: ["MyProduct"])
ios: {
  SPMPackages: [{ name: "MyPackage", libs: ["MyProduct"], path: "./packages/MyPackage" }],
}

The app builds, links, and launches on the simulator. On a device or from an archive it crashes in dyld at launch:

dyld: Library not loaded: @rpath/MyProduct.framework/MyProduct
build links @rpath/MyProduct.framework in .app/Frameworks why it runs
ns build ios (Debug, simulator) yes no Xcode adds an absolute LC_RPATH to platforms/ios/build/…/PackageFrameworks
ns build ios --release --for-device yes no it does not: rpaths are @executable_path/Frameworks and @loader_path/Frameworks only

The framework is built (with its dSYM) and simply never copied into the bundle.

Cause. SPMPbxprojService.addPackageToTarget wires a product into the XCSwiftPackageProductDependency, a PBXBuildFile with productRef, and the target's PBXFrameworksBuildPhase. Nothing adds it to the "Embed Frameworks" PBXCopyFilesBuildPhase (dstSubfolderSpec = 10). When a package product is added through Xcode's own "Frameworks, Libraries, and Embedded Content" UI, Xcode emits the same productRef twice: once in the Frameworks phase, and once in the Embed Frameworks copy phase with settings = { ATTRIBUTES = (CodeSignOnCopy, ); }. That second entry is what the CLI was missing. Xcode does not infer it from the link phase alone, as the Release build above shows.

Verified on nativescript@9.1.0-next.2026-08-19 with Xcode 26.3.

What is the new behavior?

IosSPMPackageBase gains an opt-in embed:

ios: {
  SPMPackages: [{
    name: "MyPackage",
    libs: ["MyProduct", "MyStaticProduct"],
    embed: ["MyProduct"],          // the subset of libs built as .dynamic
    path: "./packages/MyPackage",
  }],
}

For each product in embed, addPackageToTarget now:

  • finds the target's own "Embed Frameworks" copy phase (dstSubfolderSpec = 10), creating one when the target has none, the same way the link phase is resolved from the target's own buildPhases rather than any target's;
  • writes a second PBXBuildFile for the same productRef with settings = { ATTRIBUTES = (CodeSignOnCopy, ); } and comment <Product> in Embed Frameworks;
  • appends it to that phase's files, keyed on productRef so re-applying on every prepare stays idempotent.

An embed entry that is not in libs is warned about and skipped, matching how the missing-Frameworks-phase case is reported.

What the service writes for an embedded product:

305AAFFD8CBC4D6CAA90BE54 /* MyProduct in Frameworks */ = {isa = PBXBuildFile; productRef = CF378E9510E242E691E8D60B /* MyProduct */; };
A7D085F2A0404543893668AE /* MyProduct in Embed Frameworks */ = {isa = PBXBuildFile; productRef = CF378E9510E242E691E8D60B /* MyProduct */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };

Why opt-in. The CLI cannot tell a static product from a dynamic one without parsing Package.swift, and embedding a static product's placeholder breaks the build.

Nothing else changes. internal/strip-dynamic-framework-architectures.sh already walks $FRAMEWORKS_FOLDER_PATH, so embedded package frameworks get the same architecture strip and re-sign as plugin .xcframeworks.

Docs follow-up (docs.nativescript.org)

The SPM section should say that:

  • .library(type: .dynamic) products need embed, or they fail in dyld at launch on devices;
  • .library(...) without a type is static and ends up inside the app executable. A Release archive strips the executable's symbol table, so C functions reached from JS by name must then be listed for strip -s via STRIPFLAGS in build.xcconfig, or shipped as a dynamic product with embed.

Tests

test/services/ios/spm-pbxproj-service.ts:

  • an embedded product gets both a Frameworks and an Embed Frameworks build file for the same productRef, the latter with CodeSignOnCopy; a linked-only product gets no embed entry;
  • re-applying is byte-identical and does not duplicate the embed entry;
  • the Embed Frameworks phase is created when the target has none, and is not created for a package that embeds nothing;
  • an embed entry missing from libs warns and is skipped.

A Swift package product declared `.library(type: .dynamic)` was linked
into the target's Frameworks phase but never copied into the app bundle.
Debug simulator builds still ran because Xcode adds an absolute LC_RPATH
to the build directory's PackageFrameworks folder; device and archive
builds crashed in dyld at launch with "Library not loaded:
@rpath/<Product>.framework/<Product>".

Xcode records a dynamic package product added through "Frameworks,
Libraries, and Embedded Content" twice: once in the Frameworks phase
and once in the "Embed Frameworks" copy phase with CodeSignOnCopy. The
CLI only ever wrote the first entry.

`IosSPMPackageBase` gains `embed?: string[]`, the subset of `libs` to
copy into the bundle. For each, the pbxproj service writes a second
PBXBuildFile for the same productRef into the target's own "Embed
Frameworks" copy phase (dstSubfolderSpec 10), creating the phase when
the target has none. Entries are keyed on productRef so re-applying on
every prepare stays idempotent. An embed entry that is not in `libs` is
warned about and skipped.

The option is opt-in: the CLI cannot tell a static product from a
dynamic one without parsing Package.swift, and embedding a static
product's placeholder breaks the build. The existing post-build
architecture strip already walks the bundle's Frameworks folder, so
embedded package frameworks get the same treatment as plugin
frameworks.
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant