Enabling Module Stability in Swift Package Manager Projects

Enabling Module Stability in Swift Package Manager Projects

Module compiled with Swift X cannot be imported by the Swift Y compiler

Uh-oh! You've been visited by the module stability fairy. "Wait a second, didn't they fix this in Swift 5?" you ask, to no avail.

Technically, yes and no. Stable binary interfaces were introduced in Swift 5 to allow apps built with the Swift 5.0 compiler to use the Swift runtime and standard library built into the operating system, and to make sure that existing apps will remain compatible with new versions of the Swift runtime in future operating system releases.

This error refers to module stability, which was introduced in Swift 5.1 to allow Swift modules built with different compiler versions to be used together in one app. While this is possible, it's something you need to explictly enable. This is because module stability currently requires Library Evolution to also be enabled, which is a setting that ensures that newer versions of your module will not break previous releases.

If you search this error on Google, you'll get a bunch of StackOverflow answers saying "well, all you have to do is enable this in Xcode!" Pretty cool, except that I'm not using Xcode, god damnit!

Enabling Module Stability in the CLI

Before enabling module stability, make sure that you actually need it. According to Swift's official docs, library evolution support should only be used when a framework is going to be built and updated separately from its clients.

This use case is exactly how you end up with that error in the first place, so I'm assuming that you're enabling it for the right reasons.

To compile with module stability outside of Xcode, we need to pass the following flags:

  • -emit-module-interface
  • -enable-library-evolution

For SPM, you can do it by passing Xswiftc flags like this:

swift build -c release -Xswiftc -emit-module-interface -Xswiftc -enable-library-evolution

This will result in a series of .swiftinterface files being generated, which you need ship alongside the binary.

Library Evolution Implications

While this is straight-forward, enabling module stability / library evolution means you now need to be careful about how you make changes to your library. Because clients can now technically swap versions of your framework without being recompiled, you cannot introduce binary breaking changes in future updates. The compiler has features to assist you with this, so you'll know exactly when this happens.

I wish it was possible to uncouple these two concepts, because you don't need to worry about binary compability if you're also in control of the clients. This is the case of SwiftInfo, in which I need use module stability due to way the Swift inputs are read. The Swift team is aware of this, so maybe this will happen in the near future.

Module Stability Limitations

If you enable module stability, you need to make sure you don't have types with the same name as your module. Basically, interface files will reference types with the qualified format Module.Type, which you can't do in Swift if they have the same name. This is a limitation of the compiler that has not yet been resolved, and you can read more about it here.

One way to workaround this is to modify the .swiftinterface file after compiling the framework to remove all broken module references. My colleague André Alves added the following script to solve this in SwiftInfo, which depends on a framework called XcodeProj that has this issue:

sed -i '' 's/XcodeProj\.//g' XcodeProj.swiftinterface

Essentially, this transforms references to XcodeProj.XcodeProj into XcodeProj, countering the limitation.