Draft Proposal: Platform Deployment Settings

Hey,

Here is a draft proposal for setting minimum deployment target in Swift packages!

Latest draft is always available here.


Package Manager Platform Deployment Settings

Introduction

This is a proposal for adding support for specifying a per-platform minimum required deployment target in the Package.swift manifest file.

Motivation

Packages should be able to declare the minimum required platform deployment target version. SwiftPM currently uses a hardcoded value for the macOS deployment target. This creates friction for packages which want to use APIs that were introduced after the hardcoded deployment target version.

There are two ways to work around this limitation: 1) using availability checks, or 2) passing the deployment target on the command line while building the package. However, these workarounds are not ideal and the package manager should really provide a proper API for setting the required deployment target version.

Adding support for specifying deployment targets will also allow packages to restrict the platforms that they support. This is useful for packages that are never meant to be cross-platform.

Proposed solution

We propose to add the following API to PackageDescription:

/// Represents a supported platform.
struct SupportedPlatform {
    static func macOS(_ version: MacOSVersion) -> SupportedPlatform
    static func tvOS(_ version: TVOSVersion) -> SupportedPlatform
    static func iOS(_ version: IOSVersion) -> SupportedPlatform
    static func watchOS(_ version: WatchOSVersion) -> SupportedPlatform
    static let linux: SupportedPlatform

    /// Implicitly support all platforms.
    static let all: SupportedPlatform
}

extension SupportedPlatform {
    struct MacOSVersion {
        /// Create a macOS version from the given string.
        init(_ version: String)
    }

    struct TVOSVersion {
        /// Create a tvOS version from the given string.
        init(_ version: String)
    }

    struct IOSVersion {
        /// Create a iOS version from the given string.
        init(_ version: String)
    }

    struct WatchOSVersion {
        /// Create a watchOS version from the given string.
        init(_ version: String)
    }
}

/// List of known versions.
extension SupportedPlatform.MacOSVersion {
    static let v10_10: MacOSVersion
    static let v10_11: MacOSVersion
    static let v10_12: MacOSVersion
    ...
}

final class Package {

    init(
        name: String,
        platforms: [SupportedPlatform] = [.all],
        ...
    )
}

// Example usage:

let package = Package(
    name: "NIO",
    platforms: [
       .macOS(.v10_13), .iOS(.v12), .all,
    ],
    products: [
        .library(name: "NIO", targets: ["NIO"]),
    ],
    targets: [
        .target(name: "NIO"),
    ]
)

By default, a package will be assumed to support all platforms using a predefined minimum deployment version. This predefined deployment version will be the oldest deployment target version supported by the installed SDK for a given platform. One exception to this rule is macOS, for which the minimum deployment target version will start from 10.10.

Packages can explicitly choose to declare the minimum deployment target version for some platform by using the above APIs. In such cases, packages should use the SupportedPlatform.all property to declare that they want to use the default deployment target version for the platforms that they didn't explicitly specify. If a package wants to support only a limited set of platforms, they should omit the .all. This is very similar to Swift's availability attribute with the exception that the equivalent of * in the proposed APIs, .all, is not mandatory.

For example:

  • This declaration means that the package should use 10.13 on macOS, 12.0 on iOS and the default deployment target when compiled on other platforms:
    ...
    platforms: [
       .macOS(.v10_13), .iOS(.v12), .all,
    ],
    ...
  • This declaration means that the package only supports macOS and iOS and SwiftPM will emit an error if this package is used on any other platform:
    ...
    platforms: [
       .macOS(.v10_13), .iOS(.v12),
    ],
    ...

Detailed design

Changes in deployment target versions should be considered as a major breaking change for the purposes of semantic versioning since the dependees of a library package can break when a library makes such a change.

SwiftPM will emit an error if a dependency is not compatible with the top-level package's deployment version, i.e., the deployment target of dependencies must be lower than or equal to top-level package's deployment target version for a particular platform.

Each package will be compiled with the deployment target specified by it. In theory, SwiftPM can use the top-level package's deployment version to compile the entire package graph since the deployment target versions of dependencies are guaranteed to be compatible. This might even produce more efficient compilation output but it also means that the users might start seeing a lot of warnings due to use of a higher version.

Each platform version type has an initializer that can be used to construct versions that are not already provided by PackageDescription APIs. This could be because the new version was recently released or isn't appropriate to be included in the APIs (for e.g. dot versions). The version format for each platform will be documented in the API. Invalid values will be diagnosed and presented as manifest parsing errors.

SwiftPM will emit appropriate errors when an invalid value is provided for supported platforms. For e.g., an empty array, multiple declarations for the same platform, invalid version specification.

The generated Xcode project command will set the deployment target version for each platform.

Future directions

The Swift compiler supports several platforms like macOS, iOS, Linux, Windows, Android. However, the runtime availability checks currently only work for Apple platforms. It is expected that support for more platforms in the availability APIs will be added gradually as the community and support for Swift compiler grows. We think that the package manager can use a similar direction for declaring the supported platforms. We can start by allowing packages to declare support for Apple platforms with version specifications and a generic support for Linux as proposed by the above APIs. Depending on the need in the community, these APIs can be evolved over time by adding support for declaring the minimum deployment version for other platforms that Swift supports. For e.g., the API can be enhanced to declare the minimum required version for Windows and the API level for Android.

This proposal doesn't handle these two problems:

  1. Platform-specific targets or products: Consider that a package supports multiple platforms but has a product that should be only built for Linux. There is no way to express this intent and the build system may end up trying to build such targets on all platforms. A simple workaround is to use #if to conditionalize the source code of the target. Another use case comes up when you want to keep deployment target of a certain target lower than other targets in a package. A workaround is factoring out the target into its own package. A proper solution to this problem will be explored in a separate proposal, which should provide target-level settings.
  2. Platform-specific package dependencies: Similar to the above issue, a package may want to use a certain dependency only when building for a specific platform. It is currently not possible to declare such a dependency without using #if os checks in the manifest file, which doesn't interact nicely with other features like Package.resolved and the cross-compilation support. This can be solved by providing APIs to declare platform-specific package dependencies in the manifest file. This will also be explored in a separate proposal.

Impact on existing packages

Existing packages will not be impacted as the behavior described in this proposal is compatible with SwiftPM's current behavior, i.e., a package is assumed to support all platforms and SwiftPM picks a default deployment target version for the macOS platform.

The new APIs will be guarded against the tools version this proposal is implemented in. Packages that want to use this feature will need to update their tools version.

Alternatives considered

We considered making the supported platform field mandatory. We think that it would provide little value in practice and cause more friction instead. One advantage would be that SwiftPM could use that information to produce error messages when a package tries to use a dependency which is not tested on some platform. We think that is not really a big enough issue. Since Swift is cross-platform, Swift packages should generally work on all platforms that Swift supports. However, there are platform-specific packages that are only meant for certain platforms and the proposed API does provide an option to declare that intent.

We considered taking the deployment target version into the dependency resolution process to automatically find a compatible version with the top-level package. This too doesn't provide enough value in practice. Library packages generally tend to stick with a deployment target to avoid breaking their dependees. When they do bump the version, it is generally a well-thought decision and may require a semver upgrade anyway due to updates in the API.

9 Likes

I like it. With this will the package manager start to fully support non macOS and Linux targets then? If so is that going to start with only be through generated xcode projects?

1 Like

As Swift code, could package descriptions specify custom minimum version matching logic? This logic seems to be looking to satisfy the predicate base SDK >= minimum SDK. Could/should the API provide users a hook to custom matching logic? For example, supporting particular Linux distributions the package description API doesn't know about.

1 Like

+1

1 Like

Thanks for this proposal Ankit. This will be enormously useful for swift-nio-transport-services, which has a bunch of minimum SDK requirements not currently handled by Swift Package Manager.

I note your explicit decision to push out platform specific targets, products, and dependencies. I'd love to see those things addressed too, but I'm more than happy for you to take these things one at a time! With that noted, I'm a strong +1: this is a great improvement, and I'm very excited to see it.

1 Like

Seconded, this is a great improvement and will really help to solve real issues.

1 Like

The proposal aims to add APIs for platforms that are officially supported by the Swift compiler. The generated project will use the deployment target information provided for the non-macOS targets but that's not an "official" support. Generated projects are meant to provide temporary development experience inside Xcode (similar to CMake).

1 Like

I think that's an open question that should be answered as the package manager grows. If there is such a need in the community, we can explore solutions and extend the above APIs in a future proposal.

That's great to hear!

Yeah, I think those features come with their own set of complexity and it is important to break down the problem instead of trying to solve everything at once.

Awesome!

The proposal only includes capturing this information for the Package as a whole. Has any thought been given to what it will take to eventually support this information on a per target basis?

If SwiftPM is eventually going to grow to support building macOS/iOS/etc. applications, then the reality is since those can be composed of Applications, Frameworks, and Extensions. As the platforms have evolved, it isn't uncommon that that the core app supports say iOS 9.0+, but some extension is iOS 10.0+ because that's what the extension was introduced. So the App target would have one min, and an extension or two could have a higher min, and if there are frameworks common to the app and extensions or just some extensions, those framework targets could have another minimum to support a collection of the other targets.

From a quick reread, I think SupportedPlatform likely would still work to help capture this and it would just be the addition of platforms at the targets or products level to capture this data. The one catch might be if the package includes other library targets as dependents, they might have to inherit their data from the things that depend on them, but that could lead two things depending on it with different explicit platforms, and the "lowest" min OS for each support platform would have to take effect.

Anyway, while not in the current proposal, just wanted to float the concern incase these additions should get tweaked in any way to eventually support this.

Yes, this is mentioned in the future directions.

1 Like

Note that this is under active review right now: SE-0236: Package Manager Platform Deployment Settings - #7 by NeoNacho

1 Like