Enable library evolution for package dependencies

I'm building an SDK to support my app's forthcoming ExtensionKit-based API. It is a framework, and has a number of package dependencies. I'd like to make all of them available to users of the framework.

Using @_exported results in warnings about library evolution. This surprised me. Is there any way for me to enable library evolution for an Xcode target such that all its dependencies also build with it enabled? I considered using unsafeFlags, but if I understand right, that comes with some major tradeoffs.

This isn't an unreasonable feature request, but it requires some caution in the design due to the "what if two people did this" principle.

If all the dependencies are controlled by you then this is probably safe. However, if they aren't, then this is likely to be a recipe for disaster because someone else might do the same thing as you. This causes two sets of symbols to be potentially resolved, and risks producing extremely surprising runtime effects when a symbol is resolved to the wrong library.

2 Likes

Thank you for calling this out! I'd like to really make sure I understand, specifically, what you're referring to. Just to keep things simple, I'll call the package I'm exporting ExportedLib.

If my binary is a static lib, should there be any other attempt to statically link ExportedLib, it will result in a link-time failure. (This assumes that mangling would be the same, and I'm not confident this is the case for mixed evolution/non-evolution builds.)

Now, I'm interpreting what you said here as a warning of the dylib situation, where there would be duplicate ExportedLib symbols in different executables. This can produce the runtime effects you've mentioned. I've faced similar problems in non-swift code as well, where the same static lib gets linked into too many executables.

I'm aware of this danger, but unsure how to mitigate it. Is there a way to better guard/handle this, aside from just not doing this kind of thing?

The problem described is:

Let’s say your package uses PromiseKit, and you expose it to a client app.

Let’s say the client app uses another dependency called PackageBKit.

If PackageBKit also uses promisekit. And exposes it, you will have a conflict.

If the version of promisekit used by both packages is the same, it should be fine, but I’d there’s a difference, and both can’t resolve a common version there will be problems.

1 Like

I'm not sure it's viable, but I'd really like to see Swift / SPM support privatizing dependencies within my module. That is, enable the usage of dependencies without necessarily exposing them to the rest of the dependency graph, essentially revending them as if they came from my module. It may also be beneficial to allow the consumer to control this behavior as well, to allow them to force outdated libraries to encapsulate their dependencies so they can use newer versions of the same package.

This is a reasonable goal as well, but not what was asked for in this thread. The OP was quite clear about what they want (emphasis mine)

Private dependencies is a great feature, but for the use-case here (distributing a binary framework) SwiftPM and Xcode are already capable of what is needed to achieve that, by use of @_implementationOnly.

That was actually my mistake. I would definitely like to make all of the available. But, right now, I'm only using @_export for the ones that are essential for my API. I've tried to use @_implemenationOnly as much as I could. Both to limit this problem, and deal with another, much worse issue that prevents importing the module I define:

Everything about distributing a binary is painful. I explored many alternatives, and every one presents pretty terrible trade-offs. Among other requirements, I need to include an XPC service to help work around a sandboxing issue many extension clients will run into. A framework allows this to be totally transparent to consumers of my SDK.

The binary framework approach still seems like the most viable and least-bad for my particular, admittedly complex, situation.

Binaries are typically only required for one of two things. Either you want to hide the implementation of your framework for some reason or it's so large that you don't want anyone to pay the cost of building it. From your description I'm not sure where your reasoning lines up.

As I said, I would prefer not to force every user of my SDK to set up and build their own XPC service. The only way I know of to avoid this is to bundle it in a framework and the only way I know of to deliver that framework is via a binary target.

I took my inspiration for this solution from Sparkle. But I would be very happy to hear of alternatives!