SwiftPM and swappable libraries?

I've got a peculiar use-case that I was wondering if anyone has any good solutions for. The Kitura project has a dependency on a network layer... this network layer was originally provided by Kitura-net, but then NIO came along, and the previous maintainers/authors created an API compatible library called Kitura-NIO. However, they both provide the KituraNet product name. So basically right now there are two implementations of the same API, and I'd like an easy way for people adding Kitura to their project to be able to decide which implementation to use.

The previous authors came up with a clever solution using environment variables. Here's a snippet of the Package.swift of the current implementation:

var kituraNetPackage: Package.Dependency

if ProcessInfo.processInfo.environment["KITURA_NIO"] != nil {
    kituraNetPackage = .package(url: "https://github.com/Kitura/Kitura-NIO.git", from: "2.4.200")
} else {
    kituraNetPackage = .package(url: "https://github.com/Kitura/Kitura-net.git", from: "2.4.200")
}

let package = Package(
    ...
    dependencies: [
        kituraNetPackage,
        ... various dependencies ...
    ]
    ...
)

This works really well on the server. However the problem I'm facing is that if wanted to use Kitura in an iOS project with SwiftPM, there is no way to provide the KITURA_NIO environment variable to SwiftPM through Xcode. I've tried placing it in the run scripts and setting the environment variable in the scheme editor to no avail.

I thought I might be able to use SwiftPM's multi-product description, something like this:

    ...
    products: [
        .library(
            name: "Kitura",
            targets: ["Kitura"]
        ),
        .library(
            name: "KituraNIO",
            targets: ["Kitura"]
        )
    ],
    dependencies: [
        .package(url: "https://github.com/Kitura/Kitura-NIO.git", from: "2.4.200"),
        .package(url: "https://github.com/Kitura/Kitura-net.git", from: "2.4.200"),
        ... various dependencies ...
    ]
    ...

However, the problem here is that SwiftPM recognizes Kitura-NIO and Kitura-net both provide the same target and I get this error:

error: multiple products named 'KituraNet' in: Kitura-NIO, Kitura-net
error: multiple targets named 'KituraNet' in: Kitura-NIO, Kitura-net

I could create separate branches, but then I lose the version selection capability that SwiftPM offers.

Does anyone else have any good suggestions? Has anyone seen a similar problem before?

Thanks

2 Likes

It's a hack, but if you go the environment variable route, the integrated manifest loader will inherit Xcode's environment, so something like KITURA_NIO=1 open Package.swift -a Xcode should select the appropriate dependency. It's not a very good solution, but I think it's the best one available today and is used by a few of the Swift repos.

In the long term, a better solution will likely involve some sort of mechanism for feature toggles that can be exposed to package clients.

3 Likes

One caveat to keep in mind for the hack, it only works if Xcode isn't running already.

I am facing a similar problem, i have a swift package and i want to load different dependencies based on a certain condition any updates on how this can be achieved with Xcode and also using a build command to build the package

I've used this same "set an environment variable" hack. The key to getting it to work with Xcode is launching Xcode from the space that had the environment variable set: so in effect, set an environment, then use open Package.swift or the open Package.swift -a Xcode commands to propagate that environment TO Xcode.

If you click on the app icon, that only picks up your default shell/environment background, which makes it a complete PITA to set and unset variables. That said, this "hack" mechanism has been pretty and effective stable for me - and I leverage the same setup when invoking Xcodebuild or swift build / swift test commands in CI to run and validate code.

I use the following snippet in Package.swift to set feature flags, and be able to easily override them in my editor:

let interposable = "FEATURE_INTERPOSABLE"
if hasFeature(interposable, override: nil) {
  // do stuff
}

// Package API Extensions
func hasFeature(_ featureName: String, override: Bool?) -> Bool {
  let key = "SWIFT_MMIO_\(featureName)"
  let environment = ProcessInfo.processInfo.environment[key] != nil
  return override ?? environment
}