What is the risk of having dependencies in my framework with BUILD_LIBRARIES_FOR_DISTRIBUTION as false?

I'm trying to build a XCFramework that requires some dependencies. In order to properly build the framework, I must set BUILD_LIBRARIES_FOR_DISTRIBUTION as true. Now the issue is that my dependencies don't support ABI stability and can't be build with BUILD_LIBRARIES_FOR_DISTRIBUTION flag. So in order to workaround this (from 'let' property 'value' may not be initialized directly in SwiftNIOConcurrencyHelpers · Issue #1063 · grpc/grpc-swift · GitHub) I disabled this flag for all my dependencies as I'm using CocoaPods in this case. My questions are:

  1. Is it possible to disable this flag for all my dependencies using SPM?
  2. What could happen if the flag is disabled and framework's dependencies aren't ABI stable? Is it possible, that on runtime, for the client app the not-compatible version my framework dependency could be loaded? Is it possible that the framework's dependency is shared between multiple apps?

Thanks!

So long as you use @_implementationOnly import then nothing bad happens. Your framework should be statically linking its SwiftPM dependencies, so they exist only inside the framework, and @_implementationOnly prevents the details from leaking into your ABI.

Thank you for a quick response! Sounds very reassuring.

Would like to mention, that I planned to use CocoaPods for the distribution. I've noticed that, if my framework has dependencies specified in a .podspec file, then all the dependencies are explicitly added to the client's app during pod install phase. Even if I'm using @_implementationOnly import. Is it because CocoaPods adds dependencies dynamically instead of linking them statically like SPM? Should I be worried if these dependencies can't be built with the BUILD_LIBRARIES_FOR_DISTRIBUTION flag?

I'm not sure whether SPM will be an option for me, but how it works internally, I mean will the dependencies of the framework be build with the BUILD_LIBRARIES_FOR_DISTRIBUTION flag or it's not happening when they're statically linked to the framework?

I am not an expert in CocoaPods, but that sounds plausible to me. If it is then this is a problem for you, as you cannot allow the client to update your framework independently from those dependencies.

SwiftPM does not support building with BUILD_LIBRARIES_FOR_DISTRIBUTION at all. If you build your framework as an Xcodeproj and express your dependencies using Xcode's SPM support, they will not be built with BUILD_LIBRARIES_FOR_DISTRIBUTION enabled and so can be used safely.

To sum up, if I use SPM to include some dependencies and build my framework with the BUILD_LIBRARIES_FOR_DISTRIBUTION flag (also assuming that I import dependencies using @_implementationOnly), ABI stability is only on my side, the author of the framework, in part of the framework's public API between different versions.

Appreciate your effort, maybe the last question, what if my framework doesn't support ABI stability? Is it possible that my framework will be used for two different apps (with two different versions, e.g. 1.0.0 and 1.0.1) and the system will try to load only one version for these apps? And because ABI stability won't be supported, this would cause a runtime crash?

Don't set BUILD_LIBRARIES_FOR_DISTRIBUTION to true.

In general, no. Different apps are in different sandboxes and bundled frameworks (particularly those at different versions) will not be deduped across them.

The concrete impact is that the user may try to update their copy of your framework without rebuilding their code, and all kinds of bad stuff can happen. Failure to link, runtime crashes, all kinds of nastiness.

1 Like

So long as you use @_implementationOnly import then nothing bad happens. Your framework should be statically linking its SwiftPM dependencies, so they exist only inside the framework, and @_implementationOnly prevents the details from leaking into your ABI.

Actually, after some time it occurred that it's not working for me. If I add a dependency to my framework using SPM with @_implementationOnly imports and the client's app that uses my framework adds the same dependency, the client's app is receiving runtime warnings like this:

Class XXX is implemented in both A and B. One of the two will be used. Which one is undefined.

Is there any solution for this?

This sounds like a bug. If you can reproduce this in a fairly small project can you file it against bugs.swift.org?

Thank you, I followed your advice: [SR-14752] Receiving runtime warnings about duplicated dependencies while trying to integrate framework with @_implementationOnly imports · Issue #4414 · apple/swift-package-manager · GitHub.

@_implementationOnly imports don't help with Objective-C classes (or extensions with @objc methods) being defined in multiple places, and they also don't help with anything that does global lookups at runtime (like as?). While it's been a while since I worked on this feature, it wasn't ever meant for importing dependencies compiled without BUILD_LIBRARIES_FOR_DISTRIBUTION, and it wouldn't surprise me if there are other places lurking where it doesn't perfectly hide your dependency.

I'm not entirely convinced that the behavior you're seeing is a bug. Consider what should happen when you have a diamond situation:

  1. Package A exists
  2. XCFrameworks B and C both @_implementationOnly import A
  3. App D imports B and C.

If A could be dynamically linked by B and C, sure, then everything would be fine.

However, if A doesn't have library evolution enabled, there's a problem. Since B and C are individually compiled for distribution, they will both statically link A so that the XCFrameworks can be used independently. When you try to run D, you will again have two copies of A.

In theory, the copies of A could be marked A1 and A2 (so the name mangling changes) and the mixture of static linking and dynamic linking would work without errors. However, you probably don't want that solution because silently D has two copies of A, so you're paying for "just works" with code size.

The primary purpose of @_implementationOnly, from what I understand, is to make dependencies that are "physically hidden" (i.e. a downstream client cannot add a dependency on them even if they want to) also be ABI-hidden in a compiler-enforced way. However, in the example you've given, the dependency is not "physically hidden."

This presumably relates to the fact that @_implementationOnly does not affect symbol visibility. Is there any profound reason that symbols from @_implementationOnly imports shouldn't be hidden? Would that not resolve the issue?

Actually, the goal I'm trying to achieve is to create an XCFramework with dependencies and these dependencies cannot be built with the BUILD_LIBRARIES_FOR_DISTRIBUTION flag. Neither of the solutions I found work for me, so I would be very grateful if someone could help me point out any work solutions.