Issue with third party dependencies inside a XCFramework through SwiftPM

Hi,

I'm having an issue related with the SPM with xcframework and third dependencies.

First of all I built my framework project that has a facebook dependency (through SPM).

image

Then I generated the xcframework.

 xcodebuild archive \
  -scheme TestSDKFramework \
  -sdk iphoneos \
  -archivePath "archives/ios_devices.xcarchive" \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  SKIP_INSTALL=NO
  
xcodebuild archive \
  -scheme TestSDKFramework \
  -sdk iphonesimulator \
  -archivePath "archives/ios_simulators.xcarchive" \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
  SKIP_INSTALL=NO

xcodebuild -create-xcframework \
    -framework archives/ios_devices.xcarchive/Products/Library/Frameworks/TestSDKFramework.framework \
   -framework archives/ios_simulators.xcarchive/Products/Library/Frameworks/TestSDKFramework.framework \
  -output TestSDKFramework.xcframework

Uploaded to a private git the xcframework and the package that has the facebook dependency as well.

// swift-tools-version:5.3
import PackageDescription
let package = Package(
    name: "TestSDKFramework",
    platforms: [
        .iOS(.v13)
    ],
    products: [
        .library(
            name: "TestSDKFramework",
            targets: ["TestSDKFramework"])
    ],
    dependencies: [
        .package(
            url: "https://github.com/facebook/facebook-ios-sdk",
            from: "8.1.0"
        )
    ],
    targets: [
        .binaryTarget(
            name: "TestSDKFramework",
            path: "TestSDKFramework.xcframework")
    ])

And finally consumed by the final client project as SP.

But then, when I try to compile the project the Facebook module was not found.

We tried to add the Facebook dependency in the final client project as well but does not work either.

How we can work with third dependencies like facebook inside a xcframework project? Any clues?

3 Likes

Hi @brunobasas,
I'm running into almost same issue (just with AppAuth rather than Facebook dependency) - have you been able to find a solution?

Actually, for your case (defining dependency for the binary target and getting it auto-pulled) there is an improvement/workaround in your Manifest file by making use of a regular source target as wrapper and define the binary target as dependency on it:

// swift-tools-version:5.3
import PackageDescription
let package = Package(
    name: "TestSDKFramework",
    platforms: [
        .iOS(.v13)
    ],
    products: [
        .library(
            name: "TestSDKFramework",
            targets: ["TestSDKFrameworkWrapper"])    // <--- reference the wrapper here
    ],
    dependencies: [
        .package(
            url: "https://github.com/facebook/facebook-ios-sdk",
            from: "8.1.0"
        )
    ],
    targets: [
        .target(
            name: "TestSDKFrameworkWrapper",         // <--- new wrapper
            dependencies: [
                .product(name: "Facebook", package: "Facebook")
                .target(name: "TestSDKFramework")    // <-- reference the actual binary target here
            ],
            path: "Sources/TestWrapper",
            publicHeadersPath: ""
        ),
        .binaryTarget(
            name: "TestSDKFramework",
            path: "TestSDKFramework.xcframework")
    ])

(the Sources/TestWrapper directory only needs to contain an empty .swift file, cmp. the Firebase implementation as mentioned here)

However, this will probably only save the step

We tried to add the Facebook dependency in the final client project

Still, for me the "No such module" remains :frowning:

The only way I could somehow get my framework and the final client app to build properly was to add the 3rd party dependency's XCFramework folder manually via drag'n'drop rather than SPM, which probably works as the Framework Search Path is then finding the dependency properly.
Interestingly, if you make an "import Facebook" in the client's app itself the SPM dependency is found, just not from within the .swiftinterface files :confused:

Any feedback/help from the other experts on this topic possible? I also tried to play around with explicitly adding linkerSetting: [ .linkedFramework("AppAuth") ], without success ...

1 Like

The general issue here is that the Swift compiler needs to be able to see all the modules being used at build-time, either via .swiftmodule/.swiftinterface or module maps. A single XCFramework can only contain one such module, though.

There are three possible solutions:

  • You could use @_implementationOnly import for the modules from the third-party SDK if you aren't using anything from that SDK as part of your module's public interface.

  • You provide the third-party SDK separately, e.g. as its own XCFramework. The important part here is that you need to make sure that it doesn't get linked statically into your other XCFramework, otherwise you will run into duplicated symbols. For packages that means making any library products dynamic. This might require some significant effort since there could be more transitive dependencies.

  • You provide the additional module definitions out-of-band and make sure they get found at build-time by the compiler using search paths.

12 Likes

Hi @NeoNacho,

I am trying to use your second solution, or something similar. My 3d party dependencies are not under my control and thus I am having problems linking them dynamically. If I use the wrapper described above, the dependency gets always linked statically, and it can not be found on runtime.
Is there anyway of forcing the linking of the deps as dynamic? Is this only possible if the vendor of the library set type: .dynamic on their SP repo?

I did also try to wrap the 3d party dep in another package offering a type:.dynamic product but of course there is a name conflict if my package since it has the same name as the library it wraps...

In the example above I would just need a way to expose Facebook as a dynamic product, so that the app can embbed it.

Thanks!

Unfortunately, there is currently no way to influence this beyond the manifest declaration itself.

@NeoNacho I am having this exact issue and the @_implementationOnly option is not viable for me. I would love to attempt either of the other 2 options, however, I don't have a lot of background on either. Would you mind shedding some more light on those options or perhaps some helpful reading you could point me to? TIA

Hi @NeoNacho
at first a large (delayed) thank you for your response!

To be honest, I haven't yet fully understood why then the build of the iOS app fails in case both our own XCFramework as well as the external framework (here: AppAuth) are defined as SPM dependency - shouldn't the Swift compiler "see" the required AppAuth dependency when building from our .swiftinterface? I have the feeling that XCode is just not searching in the correct path, but have no idea what path could be added properly to FRAMEWORK_SEARCH_PATH.

Actually this approach seems to work for us. However, we are still unsure about any side-effects of doing so, and unfortunately the @_implementationOnly feature is not very well documented and is still in some alpha state. Even after researching some links and our internal tests we fear the risk that this brings any trouble in external apps that do integrate our framework.
Is there anything that is known that might give trouble when making use of @_implementationOnly for third-party dependencies? Would it be possible for other apps to pull in the same dependency for their own app without messing up with our framework's dependency?

Thank you so much for your helpful info, and I'd be glad if you are able to provide me some more info on this.

1 Like

I had the same problem as @brunobasas. The solution approach with @_implementationOnly helped me. But i had an additional problem where a file was created at runtime which defined public classes and methods and didn't use @_implementationOnly. With two additional lines in the shell script, I adjusted this file and now it works fine.

Thank you very much @NeoNacho!

1 Like

Hello, please can someone share an example of Package.swift file that has .xcframework with it's dependencies?
@NeoNacho @shariebg

If it adds any context to this topic, we weren't able to successfully include an XCFramework with the @_implementationOnly imports if the client app uses the same dependency. We're getting
Class XXX is implemented in both A and B. One of the two will be used. Which one is undefined runtime errors, here is a straightforward sample that replicates this issue: [SR-14752] Receiving runtime warnings about duplicated dependencies while trying to integrate framework with @_implementationOnly imports · Issue #4414 · apple/swift-package-manager · GitHub.

  1. My approach was to add the dependency in my Xcode project using Swift Package Manager.
  2. In the Swift files of the Xcode project, I imported the dependency using @_implementationOnly import <Framework>.
  3. After that, I generated an XCFramework from the Xcode project.
  4. So the dependency of the XCFramework doesn't need to be mentioned in the binary target (which is not possible anyway).

I'm having exact same issue here. Trying to incorporate 3rd party dependency (SQLite.swift) into my binaryTarget using wrapper target (firebase ios as an example: firebase-ios-sdk/Package.swift at master · firebase/firebase-ios-sdk · GitHub) like so:

let package = Package(
    name: "{framework_name}",
    products: [
        .library(
            name: "{framework_name}",
            targets: ["{framework_name}SPMTarget"]
        ),
    ],
    dependencies: [
        .package(name: "SQLite.swift", url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.13.3")
    ],
    targets: [
        .target(
            name: "{framework_name}SPMTarget",
            dependencies: [
                .target(name: "{framework_name}SPMWrapper", condition: .when(platforms: [.iOS])),
            ],
            path: "SwiftPM-PlatformExclude/{framework_name}SPMTarget"
        ),
        .target(
            name: "{framework_name}SPMWrapper",
            dependencies: [
                .target(name: "{framework_name}", condition: .when(platforms: [.iOS])),
                .product(name: "SQLite", package: "SQLite.swift")
            ],
            path: "{framework_name}SPMWrapper"
        ),
        .binaryTarget(
            name: "{framework_name}",
            path: "Framework/{framework_name}.xcframework"
        ),
    ]
)

Without using @_implementationOnly import SQLite approach I got "Missing required module 'SQLiteObjc' kind of build error as a result.
Looking at SQLite.swift's Package.swift it looks like SQLiteObjc is a submodule:

let package = Package(
    name: "SQLite.swift",
   ...
    products: [
        .library(
            name: "SQLite",
            targets: ["SQLite"]
        )
    ],
    targets: [
        .target(
            name: "SQLite",
            dependencies: ["SQLiteObjc"],
            ...
        ),
        .target(
            name: "SQLiteObjc",
            ...
        ),
...

With @_implementationOnly import SQLite there is another thing. It looks like 3rd party framework is not linked although the dependency got imported to a Xcode project.
Here is the kind of error i am getting:

Library not loaded: @rpath/SQLite.framework/SQLite
Reason: tried: '{...}/Build/Products/Debug-iphonesimulator/SQLite.framework/SQLite' (no such file)

Could someone help me with a solution?

1 Like

I'm hitting this same Library not loaded: @rpath/TransientDependency.framework/... issue using GitHub - surpher/PactSwift: A Swift version of Pact. Implements Pact Specification Version 3.

I'm building a XCFramework using Xcode 13.3+ and using it in my project. I built it in terminal and added it to my project. I'm having exactly the same issue if I build the XCFramework via Carthage (with --use-xcframeworks flag) and add it to my project.

When using Xcode 13.2.1 or older, everything seems to build and link up fine and I can use the dependency in my project(s).

Funny thing is, importing the library through SPM works just fine without any issues with any "relevant" Xcode version (that supports binaries in SPM). All transient dependencies are pulled into my project and everything builds and runs without issues.

Context:

  • Module A - a repo with a .xcodeproj (for cartahge) and Package.swift so it can be distributed via SPM and Carthage.
  • Module B - a repo wrapping huge binaries compiled from Rust codebase and is vended as a XCFramework via SPM to Module A.

Would really appreciate some help on this too? Looks like there's a few of us hitting the same/similar issue?

Seems like building a XCFramework that depends on other binaries eg .xcframework and using it in a project is broken from Xcode 13.3+?

1 Like

@NeoNacho - Thank you..!!! This was immensely helpful.

Actually this approach seems to work for us. However, we are still unsure about any side-effects of doing so, and unfortunately the @_implementationOnly feature is not very well documented and is still in some alpha state. Even after researching some links and our internal tests we fear the risk that this brings any trouble in external apps that do integrate our framework.

I have the same concerns.

I have confirmed that _implementationOnly works in my environment.
However, I have not been able to find detailed specifications or information on stability, so I am unable to determine if I can really use _implementationOnly.

thank you @NeoNacho!! this fixed my Xcode Previews :partying_face:

the fix:

@_implementationOnly import Firebase