Non-package package dependencies

I’ve made a fairly large number of packages, and I keep running into an extremely annoying problem: system libraries.

When I make a package, I want it to “just work”. If I depend on other packages, it basically does: SPM will automatically pull everything I need, and everything will work on every platform. If a requirement isn’t satisfied, the package consumer will know immediately.

But this breaks down once the package (or worse, a dependency thereof) wants to use a module that’s built-in.

Consider Foundation: while it’s expected to be on most platforms, it is not the standard library. It is quite plausible that a platform won’t have it, and SPM is helpless to convey that dependency.

Worse, consider something like SwiftUI or Combine: most platforms that run Swift can’t run those, and SPM can’t make such a requirement clear. Ideally, I’d be able to say “this product must be able to import a module with this name”. But I can’t even express that.

What’s a good solution for this? I’ve ultimately ended up just putting comments in the package manifest saying what I use, but that’s not particularly helpful for someone trying to use it as a transient dependency.

For things like SwiftUI, you can use linkedFramework to express the dependency explicitly (Apple Developer Documentation).

Foundation is a little more annoying since it comes in both library and framework form, so you'd have to use conditionals to pick the right one for the platform at hand.

Wait, what’s a library in this context relative to a framework?

Frameworks are an Apple platform concept, so things like Foundation which are frameworks on e.g. macOS, are built as libraries on other platforms, e.g. Linux. But there is no hard rule for this, e.g. libXML2 exists as a library on both Apple platforms and Linux.

Are there any potential issues with explicitly linking to things like Foundation? It doesn’t seem very common.

It’s not optimal, but one solution to this is to use conditional compilation.

#if canImport(Foundation)
import Foundation

... // your code here
#else
#error("This package requires the Foundation module.")
#endif

That doesn’t really provide any benefit beyond doing nothing. I want some way of making it clear to dependents that something like Foundation is needed.

My main problem with the status quo is that is extraordinarily difficult to tell if a dependency will break when your package runs on a different platform than your own. The current options have ambiguous meanings, and they often apply more broadly than they should.

If I want to make a package that works anywhere Swift does, how can I tell if a dependency of a dependency of a dependency doesn’t support Windows? It should be possible for the package manager to flag that, just as it flags version requirements.

There should be a way to tell if a package has any requirements beyond those needed to use SPM in the first place. It could be as simple as adding a list of strings to the package manifest and throwing a warning if a dependent doesn’t propagate the items in the list itself.

The generally-accepted approach is to try to build it. Libraries can expose different interfaces on different platforms, and only by building can the system figure out if the target platform has all of the interfaces your package requires.

Consider a library such as Foundation - some APIs are specific to Apple platforms, but that shouldn't prevent your app building on Linux if corelibs-foundation contains everything you need. Similarly, WebAssembly doesn't support everything that the Linux port does, but it also might contain enough for your project to work.

If it builds, the next step is to run the tests to validate that the result meets specification. If you're depending on libraries which have platform-specific implementations (e.g. #if os(Windows) ... #else ...), there isn't really any way to tell if the implementations are exactly the same, or if there is some small difference which your application may notice. The only way to find out is to test it.

I feel like that’s a massive problem for the future of Swift. It should at least be possible to express the possibility that a package may not be usable everywhere.

For instance, if a package doesn’t import anything that isn’t from another Swift package, and they don’t import any such thing either, it is impossible for that package to break unless there’s a bug somewhere.

Many, perhaps even most, Swift packages meet that criteria.

1 Like