How to prevent duplicate resource bundles from SPM dynamic framework in app extensions?

We use a local swift package in 6 of our app extensions. The product from the local package that we link to each app extension is a dynamic framework. And while the dynamic framework is copied into the final app bundle once, the resource bundles of each target that comprise the dynamic framework is copied into each app extension. I'd much rather have the bundles be copied into the dynamic framework once to prevent app bloat.

Here is a visualization of the issue:

.
└── MyApp.ipa/
    ├── MyApp (executable)
    ├── MyDynamicFramework_TargetA.bundle
    ├── MyDynamicFramework_TargetB.bundle
    ├── MyDynamicFramework_TargetC.bundle
    ├── Frameworks/
    │   └── MyDynamicFramework.framework/
    │       ├── TargetA
    │       ├── TargetB
    │       └── TargetC
    └── PlugIns/
        ├── Widgets.appex/
        │   ├── MyDynamicFramework_TargetA.bundle
        │   ├── MyDynamicFramework_TargetB.bundle
        │   └── MyDynamicFramework_TargetC.bundle
        ├── Intents.appex/
        │   ├── MyDynamicFramework_TargetA.bundle
        │   ├── MyDynamicFramework_TargetB.bundle
        │   └── MyDynamicFramework_TargetC.bundle
        ├── IntentsUI.appex/
        │   ├── MyDynamicFramework_TargetA.bundle
        │   ├── MyDynamicFramework_TargetB.bundle
        │   └── MyDynamicFramework_TargetC.bundle
        ├── NotificationContent.appex/
        │   ├── MyDynamicFramework_TargetA.bundle
        │   ├── MyDynamicFramework_TargetB.bundle
        │   └── MyDynamicFramework_TargetC.bundle
        ├── RichPushContent.appex/
        │   ├── MyDynamicFramework_TargetA.bundle
        │   ├── MyDynamicFramework_TargetB.bundle
        │   └── MyDynamicFramework_TargetC.bundle
        └── NotificationService.appex/
            ├── MyDynamicFramework_TargetA.bundle
            ├── MyDynamicFramework_TargetB.bundle
            └── MyDynamicFramework_TargetC.bundle

Notice that the resource bundles of Target A, B, and C are copied multiple times causing an unhealthy app size.

I'd either like the resource bundles to be copied into MyDynamicFramework or copied once into the app bundle and let the app extensions reference them.

Given the SPM + Xcode linking is a black box for the most part, how would I accomplish this?

For others, I resolved this issue by:

  1. Rewrite my own version of the synthesized Bundle.module SPM provides and adding Bundle.allBundles.first(where: { $0.resourceURL?.pathExtension == "app" })?.resourceURL as a candidate location for the resource bundle.
  2. Adding a python script to remove the duplicate bundles as a build phase.

I am facing the same problem. Any chance you can share the script that you used?

Sure, @rosskimes did you still need it?

Hello @John.liedtke, please share your script :)

Hello,

Curious if you have looked into using a binary target and xcframework for your assets inside of your dynamic framework. There's a nice write up about this from Emerge Tools, but I've admittedly never tried it: https://www.emergetools.com/blog/posts/make-your-ios-app-smaller-with-dynamic-frameworks

Binary targets are pre-compiled, ensuring that your assets bundle is already neatly packaged inside the framework. This means the compiler won't build it, and won't re-bundle it into each of your targets

By chance, has anyone in this thread filed enhancement requests for improving this Swift Package Manager functionality? For anyone new that comes upon this thread, I'd really appreciate you taking a moment to file an enhancement request through my link above, and posting the FB number here.

— Ed Ford,  DTS Engineer

While waiting for the enhancement request to SwiftPM, here's a battle-tested workaround to remove the duplicated resource bundles from app extensions (such as widgets):

Add a run script phase to run after the "Embed Foundation Extensions" build phase:

for path in "$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH"/PlugIns/*/*.bundle; do
  bundle=$(basename "$path")
  if [ -d "$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/$bundle" ]; then
    >&2 echo "Stripping $path ..."
    rm -rf "$path"
    ln -vs "../../$bundle" "$path"
  else
    >&2 echo "Not stripping $path"
  fi
done

That changes into symlinks any resource bundles that are already found by the same name under the host app. It works because app extensions are sandboxed to the same file tree as their host app.


One thing it does not solve however, is the deduplication of logic: because SwiftPM dependencies are linked statically by Xcode, any library code you include for the host app and its extensions will be duplicated in each executable binary.

I think a holistic solution by Xcode would turn the whole SwiftPM library into a dynamic framework instead, statically linked by the app and its extensions. The resource bundles could then probably exist inside the framework rather than next to it.

How to prevent duplicate resource bundles from SPM dynamic framework in app extensions?
 
 
Q