Prevent using Apple-only frameworks in cross-platform Swift packages

Hello everyone,

I'm using the TabularData framework inside a Swift package. I developed the package on macOS, and everything works as expected there. However, when I try to compile the code on Ubuntu, I get the following error:

No such module 'TabularData'

I believe this is because the TabularData framework is only available on Apple platforms.

I have two questions:

  1. How could I have known that TabularData is exclusive to Apple platforms?
  2. Since the package should also compile on Linux, is there a way to make code fail to compile on macOS if it includes frameworks that won't compile on Linux?

To catch these, you really need to just build with a custom SDK which excludes the system frameworks, but that would also include any frameworks that you may need (unless you are building everything from scratch and are not using anything from the system).

The only feasible, reliable way to do this that I can think of is to just build for Linux. This may be in the form of building on Linux, in a container, VM, or potentially if Linux is the only target that you care about, cross compilation with the appropriate sysroot.

If that really is the only way to do it, I find it a bit unfortunate.
I had expected the platforms parameter to control situations like this, but it doesn't seem to.
It also feels strange that the package otherwise builds and runs fine, even though I specified .macOS(.v13) while working on Linux.
I’m still not sure what effect platforms actually has then.

Is this really different from the way that any other language does it? If you need to know if an arbitrary Python library works on an arbitrary platform, the only way to know for sure is to build its dependencies and run its tests on that platform.

platforms clearly has a confusing name, it should be minimumSupportedPlatformVersion. It only has an effect for versioned platforms, which in SwiftPM sense is currently only Darwin, as that's the only one that has ABI stability.

1 Like

That's true, but I think the problem is also compounded by SwiftPM's lax treatment of system frameworks. One potential guardrail that would be nice would be for SwiftPM to require the package manifest to explicitly declare any dependencies on libraries/frameworks that come the system or the toolchain, just as it does for third-party packages. That would make it much harder for a dependency to slip in just because it happens to be available on the development platform.

Of course, that would introduce its own brand of complexity for anyone who needs to import something like the platform/libc library, which today has a different name on pretty much every group of platforms that Swift supports (Darwin, Glibc, Android, CRT, Musl, WASILibc, ...).

1 Like

I wouldn’t put all the blame on SwiftPM here. As Saleem mentioned, we build against the MacOS SDK when the –target specifies that. The MacOS SDK is where we pick up system frameworks, such as Foundation. There are a lot of frameworks right next to it so we pick them all up.

SwiftPM has no knowledge of which of these frameworks exist on other platforms and I don’t think it should have knowledge of any platform at that level of detail. It should work the same no matter what the platform is. That’s not necessarily true today, but it is an ideal we should reach for especially as we grow to new platforms and try to do so with minimal if any code changes in the toolchain.

2 Likes

SPM oddly enough might actually be amongst the few build systems that can do something about this. You could use the sandboxing to prevent access to the system frameworks by default and require you to opt-in. But this is not something that should be on SPM, it has the mechanism for this, but this should be something that the user would actively configure.

Just a vehement agreement on this - this is well beyond the scope of what SPM or the build system should know about.

To be clear, I'm not suggesting that SwiftPM have any specific knowledge of platforms or modules available on them, but that it provide a general way for users to express that a target is using a dependency that comes from some external location on the system instead of having the compiler just pick it up by default because it happens to find it via the default search paths.

There's already sort of something like this via Target.systemLibrary, but that's meant for creating a new target that wraps a specific library, rather than just describing dependencies of other targets.

Part of the build system's job is dependency validation, so I think it would be very reasonable for SwiftPM to allow users to opt in to a stricter mode where even system dependencies are validated in the same way that it already validates package dependencies (e.g., you can't import a module that you don't depend on, which includes those that are transitively available through one of your dependencies). I haven't looked at the implementation too closely but I suspect that the scan-dependencies logic in SwiftPM would already support this, with it just needing a way to express the system dependencies in the manifest.

2 Likes

Similar problem exists the other way too. When working on Linux, you get no feedback from the compiler if a certain API is not available on the oldest macOS you want to support.

The best you can do is to set up CI to run swift build or swift test on both macOS and Linux for every PR.

1 Like