From 49bfbe7742b05beadd5f55295649b1d04383f45a Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 4 Mar 2024 16:52:35 +0000 Subject: [PATCH] Package traits --- proposals/NNNN-swiftpm-package-traits.md | 504 +++++++++++++++++++++++ 1 file changed, 504 insertions(+) create mode 100644 proposals/NNNN-swiftpm-package-traits.md diff --git a/proposals/NNNN-swiftpm-package-traits.md b/proposals/NNNN-swiftpm-package-traits.md new file mode 100644 index 00000000000..c8e7042e028 --- /dev/null +++ b/proposals/NNNN-swiftpm-package-traits.md @@ -0,0 +1,504 @@ +# Package traits + +* Proposal: [SE-NNNN](NNNN-swiftpm-package-traits.md) +* Authors: [Franz Busch](https://github.com/FranzBusch) +* Review Manager: TBD +* Status: **Implemented** https://github.com/apple/swift-package-manager/pull/7392 + +## Introduction + +Over the past years the package ecosystem has grown tremendously in both the +amount of packages and the functionality that individual packages offer. +Additionally, Swift is being used in more environments such as embedded systems +or Wasm. This proposal aims to give package authors a new tool to conditionalize +the features they offer and the dependencies that they use. + +## Motivation + +There are various motivating use-cases where package authors might want to +express configurable compilation or optional dependencies. This section is going +to list a few of those use-cases. + +### Minimizing build times and binary size + +Some packages offer different but adjacent functionality such as the +`swift-collections` package. To reduce build time and binary size impact +`swift-collections` offers multiple different products and users can choose +which one they need. This works however, it comes with the downside that if the +implementation wants to share code between the different modules it needs to +create internal targets. Furthermore, the user has to declare a dependency on +different products and import each product module individually. + +### Pluggable dependencies + +Some packages want to make it configurable what underlying technology is used. +The [Swift OpenAPIGenerator](https://github.com/apple/swift-openapi-generator) +for example is capable of running on top of `URLSession`, `AsyncHTTPClient`, +`Hummingbird` or `Vapor`. To avoid bringing all of those potential dependencies +into every adopters binary, the project has created individual repositories for +each transport. This achieves the goal of making the dependencies optional; +however, it requires users to discovery those adjacent repositories and add +additional dependencies to their project. + +### Configurable behavior + +Packages often want to cater to multiple ecosystems such as the iOS or the +server ecosystem. While most of the technologies are shared between ecosystems +there are often some platform specific behaviors/libraries that one might use. +For example, on Apple's platforms `OSLog` is the canonical logging system +whereas the server ecosystem is mostly using `swift-log`. However, there are +some users that prefer to use `swift-log` on Apple's platforms which means +libraries and applications cannot use platform compiler conditionals. + +### Replacing environment variables in Package manifests + +A lot of packages are using environment variables in their `Package.swift` to +configure their package. This has various reasons such as optional dependencies +or setting certain defines for local development. Using environment variables +inside `Package.swift` is not officially supported and with stricter sandboxing +rules might break in the future. + +## Proposed solution + +This proposal introduces a new configuration for packages called _package +traits_. Package authors can define a set of traits in their `Package.swift` +that their package offers which provide a way to express conditional compilation +and optional dependencies. Furthermore, a set of default enabled traits can be +specified. + +```swift +let package = Package( + name: "Example", + traits: [ + "Foo", + Trait( + name: "Bar", + enabledTraits: [ // Other traits that are enabled when this trait is being enabled + "Foo" + ] + ) + ], + defaultTraits: [ // The set of default enabled traits + "Foo", + ], + /// ... +) +``` + +When depending on a package a set of enabled traits can be passed and whether the +default set of traits are disabled. + +```swift +dependencies: [ + .package( + url: "https://github.com/Org/SomePackage.git", + from: "1.0.0", + traits: Package.Dependency.Trait( + enabledTraits: ["SomeTrait"], + disableDefaultTraits: true + ) + ), +] +``` + +Another common scenario is to enable a trait of a dependency only when a trait +of the package is enabled. The below example enables the `SomeOtherTrait` when +the `Foo` trait of this package is enabled. + +```swift +dependencies: [ + .package( + url: "https://github.com/Org/SomePackage.git", + from: "1.0.0", + traits: Package.Dependency.Trait( + enabledTraits: [ + "SomeTrait", + EnabledTrait("SomeOtherTrait", condition: .when(traits: ["Foo"])), + ] + ) + ), +] +``` + +Conditional dependencies are specified per target and extend the current +`condition` syntax which is used for specifying platform dependent dependencies. + +```swift +targets: [ + .target( + name: "SomeTarget", + dependencies: [ + .product( + name: "SomeProduct", + package: "SomePackage", + condition: .when(traits: ["Foo"]) + ), + ] + ) +] +``` + +Lastly, code can be conditionally compiled by checking if a trait is enabled. +This can be used for both optional dependencies by surrounding the `import` +statements in a trait check and for regular code where you want to modify its +behaviour depending on the enabled traits. + +```swift +#if TRAIT_Foo +import SomeDependency +#endif + +func hello() { + #if TRAIT_Foo + Foo.hello() + #else + print("Hello") + #endif +} +``` + +At this point, it is important to talk about the trait unification across the +entire dependency graph. After dependency resolution the union of enabled traits +per package is calculated. This is then used to determine both the enabled +optional dependencies and the enabled traits for the compile time checks. Since +the enabled traits of a dependency are specified on a per package level and not +from the root of the tree, any combination of enabled traits must be supported. +A consequence of this is that all traits **must** be additive. Enabling a trait +**must never** disable functionality i.e. remove API or lead to any other +SemVer-incompatible change. + +## Detailed design + +This proposal extends the current `PackageDescription` APIs by introducing the +following new `Trait` type. + +```swift +/// A struct representing a package's trait. +/// +/// Traits can be used for expressing conditional compilation and optional dependencies. +/// +/// - Important: Traits must be strictly additive and enabling a trait **must not** remove API. +public struct Trait: Hashable, ExpressibleByStringLiteral { + /// The trait's canonical name. + /// + /// This is used when enabling the trait or when referring to it from other modifiers in the manifest. + public var name: String + + /// A set of other traits of this package that this trait enables. + public var enabledTraits: Set + + /// Initializes a new trait. + /// + /// - Parameters: + /// - name: The trait's canonical name. + /// - enabledTraits: A set of other traits of this package that this trait enables. + public init(name: String, enabledTraits: Set = []) + + public init(stringLiteral value: StringLiteralType) +} +``` + +The `Package` class is extended to define traits and default traits: + +```swift +public final class Package { + // ... + + /// The set of traits of this package. + public var traits: Set + + /// The set of traits enabled by default when this package is used as a dependency. + public var defaultTraits: Set + + /// Initializes a Swift package with configuration options you provide. + /// + /// - Parameters: + /// - name: The name of the Swift package, or `nil` to use the package's Git URL to deduce the name. + /// - defaultLocalization: The default localization for resources. + /// - platforms: The list of supported platforms with a custom deployment target. + /// - pkgConfig: The name to use for C modules. If present, Swift Package Manager searches for a + /// `.pc` file to get the additional flags required for a system target. + /// - providers: The package providers for a system target. + /// - products: The list of products that this package makes available for clients to use. + /// - traits: The set of traits of this package. + /// - defaultTraits: The set of traits enabled by default when this package is used as a dependency. + /// - dependencies: The list of package dependencies. + /// - targets: The list of targets that are part of this package. + /// - swiftLanguageVersions: The list of Swift versions with which this package is compatible. + /// - cLanguageStandard: The C language standard to use for all C targets in this package. + /// - cxxLanguageStandard: The C++ language standard to use for all C++ targets in this package. + public init( + name: String, + defaultLocalization: LanguageTag? = nil, + platforms: [SupportedPlatform]? = nil, + pkgConfig: String? = nil, + providers: [SystemPackageProvider]? = nil, + products: [Product] = [], + traits: Set = [], + defaultTraits: Set = [], + dependencies: [Dependency] = [], + targets: [Target] = [], + swiftLanguageVersions: [SwiftVersion]? = nil, + cLanguageStandard: CLanguageStandard? = nil, + cxxLanguageStandard: CXXLanguageStandard? = nil + ) +} +``` + +Furthermore, a new `Traits` type is introduced that can be used +to configure the traits of a dependency. + +```swift +extension Package.Dependency { + /// A struct representing the trait configuration of a dependency. + public struct Traits { + /// A struct representing an enabled trait of a dependency. + public struct EnabledTrait: Hashable, ExpressibleByStringLiteral { + /// A condition that limits the application of a dependencies trait. + public struct Condition: Hashable { + /// The set of traits that enable the dependencies trait. + let traits: Set? + + /// Creates a package dependency trait condition. + /// + /// - Parameter traits: The set of traits that enable the dependencies trait. If any of the traits are enabled on this package + /// the dependencies trait will be enabled. + @available(_PackageDescription, introduced: 9999) + public static func when( + traits: Set + ) -> Self? + } + + /// The name of the enabled trait. + public var name: String + + /// The condition under which the trait is enabled. + public var condition: Condition? + + /// Initializes a new enabled trait. + /// + /// - Parameters: + /// - name: The name of the enabled trait. + /// - condition: The condition under which the trait is enabled. + public init(name: String, condition: Condition? = nil) + + public init(stringLiteral value: StringLiteralType) + } + + /// The enabled traits of the dependency. + public var enabledTraits: Set + + /// Wether the default traits are disabled. + public var disableDefaultTraits: Bool + + /// Initializes a new traits configuration. + /// + /// - Parameters: + /// - enabledTraits: The enabled traits of the dependency. + /// - disableDefaultTraits: Wether the default traits are disabled. Defaults to `false`. + public init( + enabledTraits: Set, + disableDefaultTraits: Bool = false + ) + } +} +``` + +The dependency APIs are then extended with new variants that take a `Trait` parameter: + +```swift +extension Package.Dependency { + // MARK: Path + + public static func package( + path: String, + traits: Traits + ) -> Package.Dependency + + public static func package( + name: String, + path: String, + traits: Traits + ) -> Package.Dependency + + // MARK: Source repository + + public static func package( + url: String, + from version: Version, + traits: Traits + ) -> Package.Dependency + + public static func package( + url: String, + branch: String, + traits: Traits + ) -> Package.Dependency + + public static func package( + url: String, + revision: String, + traits: Traits + ) -> Package.Dependency + + public static func package( + url: String, + _ range: Range, + traits: Traits + ) -> Package.Dependency + + public static func package( + url: String, + _ range: ClosedRange, + traits: Traits + ) -> Package.Dependency + + public static func package( + url: String, + exact version: Version, + traits: Traits + ) -> Package.Dependency + + // MARK: Registry + + public static func package( + id: String, + from version: Version, + traits: Traits + ) -> Package.Dependency + + public static func package( + id: String, + exact version: Version, + traits: Traits + ) -> Package.Dependency + + public static func package( + id: String, + _ range: Range, + traits: Traits + ) -> Package.Dependency + + public static func package( + id: String, + _ range: ClosedRange, + traits: Traits + ) -> Package.Dependency +} +``` + +Lastly, traits can also be used to conditionalize `SwiftSettings`, `CSettings`, +`CXXSettings` and `LinkerSettings`. For this the `BuildSettingCondition` is extended. + +```swift +/// Creates a build setting condition. +/// +/// - Parameters: +/// - platforms: The applicable platforms for this build setting condition. +/// - configuration: The applicable build configuration for this build setting condition. +/// - traits: The applicable traits for this build setting condition. +public static func when( + platforms: [Platform]? = nil, + configuration: BuildConfiguration? = nil, + traits: Set? = nil +) -> BuildSettingCondition { + precondition(!(platforms == nil && configuration == nil)) + return BuildSettingCondition(platforms: platforms, config: configuration, traits: nil) +} +``` + +### Default traits + +Default traits allow package authors to define a set of traits that they think +cater to the majority use-cases of the package. When choosing the initial +default traits or adding a new default trait it is important to consider that +removing a default trait is a SemVer major change since it can potentially +remove APIs. + +### Trait specific command line options for `swift build/run` + +When executing one of `swift build/run` options can be passed to control which +traits for the root package are enabled: + +- `--traits` _TRAITS_: Enables the passed traits of the package. Multiple traits + can be specified by providing a comma separated list e.g. `--traits + Trait1,Trait2`. +- `--enable-all-traits`: Enables all traits of the package. +- `--disable-default-traits`: Disables all default traits of the package. + +### Trait namespaces + +Trait names are namespaced per package; hence, multiple packages can define the +same trait names. Moreover, it is expected that multiple packages define the +same trait and conditionally enable the equivalent named trait in their +dependencies. + +### Trait limitations + +To prevent abuse, limit the complexity and make sure it integrates with the +compiler a few limitations are imposed. + +#### Number of traits + +[Other +ecosystems](https://blog.rust-lang.org/2023/10/26/broken-badges-and-23k-keywords.html) +have shown that a large number of traits can have significant impact on +registries and dependency managers. To avoid such a scenario an initial maximum +number of 300 defined traits per package is imposed. This can be revisited later +once traits have been used in the ecosystem extensively. + +### Allowed characters for trait names + +Since traits can show up both in the `Package.swift` and in source code when +checking if a trait is enabled, the allowed characters for a trait name have to +be restricted. The following rules are enforced on trait names: + +- The first character must be a [Unicode XID start + character](https://unicode.org/reports/tr31/#Figure_Code_Point_Categories_for_Identifier_Parsing) + (most letters), a digit, or `_`. +- Subsequent characters must be a [Unicode XID continue + character](https://unicode.org/reports/tr31/#Figure_Code_Point_Categories_for_Identifier_Parsing) + (a digit, `_`, or most letters), `-`, or `+`. + +## Impact on existing packages + +There is no impact on existing packages. Any package can start adopting package +traits but in doing so **must not** move existing API behind new traits. + +## Future directions + +### Consider traits during dependency resolution + +The implementation to this proposal only considers traits **after** the +dependency resolution when constructing the module graph. This is inline with +how platform specific dependencies are currently handled. In the future, both +platform specific dependencies and traits can be taken into consideration during +dependency resolution. + +### Integrated compiler trait checking + +The current proposal passes enabled traits via custom defines to the compiler +and code can check it using regular define checks (`#if DEFINE`). In the future, +we can extend the compiler to make it aware of package traits to allows syntax +like `#if trait(FOO)` or implement an extensible configuration macro similar to +Rust's `cfg` macro. + +## Alternatives considered + +### Different naming + +During the implementation and writing of the proposal different names for +_package traits_ have been considered such as _package features_. A lot of the +other considered names have other meanings in the language already. + +## Prior art + +Other dependency managers have similar features to control optional dependencies +and conditional compilation. + +- [Cargo](https://doc.rust-lang.org/cargo/) has [optional features](https://doc.rust-lang.org/cargo/reference/features.html) that allow conditional compilation and optional dependencies. +- [Maven](https://maven.apache.org/) has [optional dependencies](https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html). +- [Gradle](https://gradle.org/) has [feature variants](https://docs.gradle.org/current/userguide/feature_variants.html) that allow conditional compilation and optional dependencies. +- [Go](https://golang.org/) has [build constraints](https://golang.org/pkg/go/build/#hdr-Build_Constraints) which can conditionally include a file. +- [pip](https://pypi.org/project/pip/) dependencies can have [optional dependencies and extras](https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#optional-dependencies). +- [Hatch](https://hatch.pypa.io/latest/) offers [optional dependencies](https://hatch.pypa.io/latest/config/metadata/#optional) and [features](https://hatch.pypa.io/latest/config/dependency/#features). \ No newline at end of file