From bdef9b414f5afeeb646d41d1fca0f2a53176c624 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 4 Mar 2024 16:52:35 +0000 Subject: [PATCH 1/6] Package traits --- proposals/NNNN-swiftpm-package-traits.md | 583 +++++++++++++++++++++++ 1 file changed, 583 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 0000000000..0f997aa5db --- /dev/null +++ b/proposals/NNNN-swiftpm-package-traits.md @@ -0,0 +1,583 @@ +# Package traits + +* Proposal: [SE-NNNN](NNNN-swiftpm-package-traits.md) +* Authors: [Franz Busch](https://github.com/FranzBusch), [Max Desiatov](https://github.com/MaxDesiatov) +* Review Manager: TBD +* Status: **Work in progress implementation** 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. + +### Experimental APIs + +Some packages want to introduce new functionality without yet committing to a +stable public API Currently, those modules and APIs are often underscored or +specifically annotated. While this approach works it comes with downsides such +as hiding the APIs in code completion. + +## 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", + ] + ) + Trait( + name: "FooBar", + isDefault: true, + enabledTraits: [ + "Foo", + "Bar", + ] + ) + ], + /// ... +) +``` + +When depending on a package all default traits are enabled. However, the enabled +traits can be customized by passing a set of enabled traits when declaring the +dependency. When specifying the enabled dependencies the `.defaults` trait can +be passed which will enable all default traits of the dependency. The below +example enables all default traits and the additional `SomeTrait` of the +package. + +```swift +dependencies: [ + .package( + url: "https://github.com/Org/SomePackage.git", + from: "1.0.0", + traits: [ + .defaults, + "SomeTrait" + ] + ), +] +``` + +To disable all traits including the default traits an empty set can be passed. + +```swift +dependencies: [ + .package( + url: "https://github.com/Org/SomePackage.git", + from: "1.0.0", + traits: [] // All traits are disabled + ), +] +``` + +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 Foo +import SomeDependency +#endif + +func hello() { + #if Foo + Foo.hello() + #else + print("Hello") + #endif +} +``` + +## 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 boolean indicating wether the trail is enabled by default. + public var isDefault: Bool + + /// 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. + /// - isDefault: A boolean indicating wether the trail is enabled by default. + /// - enabledTraits: A set of other traits of this package that this trait enables. + public init(name: String, isDefault: Bool, enabledTraits: Set = []) + + /// Initializes a new trait. + /// + /// This trait is disabled by default and enables no other trait of this package. + public init(stringLiteral value: StringLiteralType) +} +``` + +The `Package` class is extended to define a set of traits: + +```swift +public final class Package { + // ... + + /// The set of traits of this package. + public var traits: 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. + /// - 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 = [], + dependencies: [Dependency] = [], + targets: [Target] = [], + swiftLanguageVersions: [SwiftVersion]? = nil, + cLanguageStandard: CLanguageStandard? = nil, + cxxLanguageStandard: CXXLanguageStandard? = nil + ) +} +``` + +Furthermore, a new `Package.Dependency.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? + } + + /// Enables all default traits of a package. + static var defaults: EnabledTrait + + /// 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 + + /// 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: Set + ) -> Package.Dependency + + public static func package( + name: String, + path: String, + traits: Set + ) -> Package.Dependency + + // MARK: Source repository + + public static func package( + url: String, + from version: Version, + traits: Set + ) -> Package.Dependency + + public static func package( + url: String, + branch: String, + traits: Set + ) -> Package.Dependency + + public static func package( + url: String, + revision: String, + traits: Set + ) -> Package.Dependency + + public static func package( + url: String, + _ range: Range, + traits: Set + ) -> Package.Dependency + + public static func package( + url: String, + _ range: ClosedRange, + traits: Set + ) -> Package.Dependency + + public static func package( + url: String, + exact version: Version, + traits: Set + ) -> Package.Dependency + + // MARK: Registry + + public static func package( + id: String, + from version: Version, + traits: Set + ) -> Package.Dependency + + public static func package( + id: String, + exact version: Version, + traits: Set + ) -> Package.Dependency + + public static func package( + id: String, + _ range: Range, + traits: Set + ) -> Package.Dependency + + public static func package( + id: String, + _ range: ClosedRange, + traits: Set + ) -> 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) +} +``` + +### Trait unification + +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. + +### 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-incompatible** 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 an expected scenario that multiple packages +define the same trait name 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 are +restricted to [legal Swift +identifier](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/summaryofthegrammar/). +Hence, 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 `+`. +- `default` and `defaults` (in any letter casing combination) are not allowed as + trait names to avoid confusion with default traits. + +## 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. Even if +the trait is a enabled by default any consumer might have already disabled all +default traits; hence, moving API behind a new default trait could potentially +break them. + +## 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 to avoid fetching an optional dependency that is not +enabled by a trait. Changing this **doesn't** require a Swift evolution proposal +since it is just an implementation detail of how dependency resolution currently +works. + +### 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. + +### Enabled trait compile time checking + +Since trait unification is done for every package in the graph during build time +the information which module enabled which trait of its dependencies is lost. +Rather the build system start to build from the bottom up while setting all the +compiler defines for the unified traits. As a consequence it might be that a +package accidentally uses an API from a dependency which is guarded by a trait +that another package in the graph has enabled. Since the traits that any one +package in the graph enables on its dependencies are not considered part of the +semantic version, it can happen that disabling a trait could result in breaking a +build. In the future, we could integrate trait checking further into the compiler +where it understands if an API is only available if a certain trait is set. + +> Cargo currently [treats this +similar](https://users.rust-lang.org/t/is-disabling-features-of-a-dependency-considered-a-breaking-change/94302/2) +and doesn't consider disabling a cargo feature a breaking change. + +### Different default traits depending on platform + +A future evolution could allow to mark traits as default depending on the +platform that the package is build on. This would allow packages such as the +`swift-openapi-generator` to default the used transport depending on the +platform which makes it even easier to offer users the best out of box +experience. This is left as a future evolution since it intersects interestingly +with the future direction "Consider traits during dependency resolution". If +default traits depend on the target build platform then this must be an input to +the dependency resolution. + +## Alternatives considered + +### Different naming + +During the implementation and writing of the proposal different names for +_package traits_ have been considered such as: +- Package features +- Package optional features +- Package options +- Package parameters +- Package flags +- Package configuration + +A lot of the other considered names have other meanings in the language already. +For example `feature` is already used in expressing compiler feature via +`enable[Upcoming|Experimental]Feature` and the `hasFeature` check. + +## 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 From c6b8015d87902719263acd7aca896f3857b14049 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 28 Jun 2024 17:32:42 +0100 Subject: [PATCH 2/6] Update proposal --- proposals/NNNN-swiftpm-package-traits.md | 274 +++++++++++++++-------- 1 file changed, 176 insertions(+), 98 deletions(-) diff --git a/proposals/NNNN-swiftpm-package-traits.md b/proposals/NNNN-swiftpm-package-traits.md index 0f997aa5db..893136e328 100644 --- a/proposals/NNNN-swiftpm-package-traits.md +++ b/proposals/NNNN-swiftpm-package-traits.md @@ -19,16 +19,6 @@ 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. @@ -78,31 +68,30 @@ let package = Package( name: "Example", traits: [ "Foo", - Trait( + .trait( name: "Bar", enabledTraits: [ // Other traits that are enabled when this trait is being enabled "Foo", ] ) - Trait( + .trait( name: "FooBar", - isDefault: true, enabledTraits: [ "Foo", "Bar", ] - ) + ), + .default(enabledTraits: ["Foo"]), // Defines all the default enabled traits ], /// ... ) ``` -When depending on a package all default traits are enabled. However, the enabled +When depending on a package the default trait is enabled. However, the enabled traits can be customized by passing a set of enabled traits when declaring the -dependency. When specifying the enabled dependencies the `.defaults` trait can -be passed which will enable all default traits of the dependency. The below -example enables all default traits and the additional `SomeTrait` of the -package. +dependency. When specifying the enabled traits of the dependencies the +`.default` trait can be passed which will enable the default trait. The below +example enables the default trait and the additional `SomeTrait` of the package. ```swift dependencies: [ @@ -110,14 +99,14 @@ dependencies: [ url: "https://github.com/Org/SomePackage.git", from: "1.0.0", traits: [ - .defaults, + .default, "SomeTrait" ] ), ] ``` -To disable all traits including the default traits an empty set can be passed. +To disable all traits including the default trait an empty set can be passed. ```swift dependencies: [ @@ -138,12 +127,9 @@ 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"])), - ] - ) + traits:[ + .trait("SomeOtherTrait", condition: .when(traits: ["Foo"])), + ] ), ] ``` @@ -194,16 +180,26 @@ following new `Trait` type. /// 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 { + /// Declares the default traits for this package. + public static func `default`(enabledTraits: Set) -> Self + /// The trait's canonical name. /// /// This is used when enabling the trait or when referring to it from other modifiers in the manifest. + /// + /// 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 `+`. + /// - `default` and `defaults` (in any letter casing combination) are not allowed as trait names to avoid confusion with default traits. public var name: String - /// A boolean indicating wether the trail is enabled by default. - public var isDefault: Bool + /// The trait's description. + /// + /// Use this to explain what functionality this trait enables. + public var description: String? /// A set of other traits of this package that this trait enables. public var enabledTraits: Set @@ -212,14 +208,30 @@ public struct Trait: Hashable, ExpressibleByStringLiteral { /// /// - Parameters: /// - name: The trait's canonical name. - /// - isDefault: A boolean indicating wether the trail is enabled by default. + /// - description: The trait's description. /// - enabledTraits: A set of other traits of this package that this trait enables. - public init(name: String, isDefault: Bool, enabledTraits: Set = []) + public init( + name: String, + description: String? = nil, + enabledTraits: Set = [] + ) /// Initializes a new trait. /// /// This trait is disabled by default and enables no other trait of this package. public init(stringLiteral value: StringLiteralType) + + /// Initializes a new trait. + /// + /// - Parameters: + /// - name: The trait's canonical name. + /// - description: The trait's description. + /// - enabledTraits: A set of other traits of this package that this trait enables. + public static func trait( + name: String, + description: String? = nil, + enabledTraits: Set = [] + ) -> Trait } ``` @@ -265,61 +277,54 @@ public final class Package { } ``` -Furthermore, a new `Package.Dependency.Traits` type is introduced that can be used +Furthermore, a new `Package.Dependency.Trait` 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? - } - - /// Enables all default traits of a package. - static var defaults: EnabledTrait - - /// 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. + /// A struct representing an enabled trait of a dependency. + public struct Trait: Hashable, Sendable, ExpressibleByStringLiteral { + /// Enables the default traits of a package. + public static let default: Self + + /// A condition that limits the application of a dependencies trait. + public struct Condition: Hashable, Sendable { + /// Creates a package dependency trait condition. /// - /// - 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) + /// - 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. + public static func when( + traits: Set + ) -> Self? } - /// The enabled traits of the dependency. - public var enabledTraits: Set + /// The name of the enabled trait. + public var name: String - /// Initializes a new traits configuration. + /// The condition under which the trait is enabled. + public var condition: Condition? + + /// Initializes a new enabled trait. /// /// - Parameters: - /// - enabledTraits: The enabled traits of the dependency. - /// - disableDefaultTraits: Wether the default traits are disabled. Defaults to `false`. + /// - name: The name of the enabled trait. + /// - condition: The condition under which the trait is enabled. public init( - enabledTraits: Set, - disableDefaultTraits: Bool = false + name: String, + condition: Condition? = nil ) + + public init(stringLiteral value: StringLiteralType) + + /// Initializes a new enabled trait. + /// + /// - Parameters: + /// - name: The name of the enabled trait. + /// - condition: The condition under which the trait is enabled. + public static func trait( + name: String, + condition: Condition? = nil + ) -> Trait } } ``` @@ -332,13 +337,13 @@ extension Package.Dependency { public static func package( path: String, - traits: Set + traits: Set ) -> Package.Dependency public static func package( name: String, path: String, - traits: Set + traits: Set ) -> Package.Dependency // MARK: Source repository @@ -346,37 +351,37 @@ extension Package.Dependency { public static func package( url: String, from version: Version, - traits: Set + traits: Set ) -> Package.Dependency public static func package( url: String, branch: String, - traits: Set + traits: Set ) -> Package.Dependency public static func package( url: String, revision: String, - traits: Set + traits: Set ) -> Package.Dependency public static func package( url: String, _ range: Range, - traits: Set + traits: Set ) -> Package.Dependency public static func package( url: String, _ range: ClosedRange, - traits: Set + traits: Set ) -> Package.Dependency public static func package( url: String, exact version: Version, - traits: Set + traits: Set ) -> Package.Dependency // MARK: Registry @@ -384,25 +389,25 @@ extension Package.Dependency { public static func package( id: String, from version: Version, - traits: Set + traits: Set ) -> Package.Dependency public static func package( id: String, exact version: Version, - traits: Set + traits: Set ) -> Package.Dependency public static func package( id: String, _ range: Range, - traits: Set + traits: Set ) -> Package.Dependency public static func package( id: String, _ range: ClosedRange, - traits: Set + traits: Set ) -> Package.Dependency } ``` @@ -435,10 +440,29 @@ 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 +A consequence of this is that all traits **should** be _additive_. Enabling a trait +**should not** disable functionality i.e. remove API or lead to any other **SemVer-incompatible** change. +#### Mutally exclusive traits + +Some rare use-cases may want mutally exclusive traits which are incompatible to +be enabled at the same time. This should be avoided if possible because it +requires the whole dependency graph to coordinate on what trait to enable. In +the rare case where mutually exclusive traits are used consider adding a +compiler error to detect this during build time. + +```swift +#if Trait1 && Trait2 +#error("Trait1 and Trait2 are mutuall exclusive") +#endif +``` + +A few options to avoid mutually exclusive traits: +- Separate the code into multiple packages +- Choose one trait over the other when possible +- Use platform checks `#if os(Windows)` when possible + ### Default traits Default traits allow package authors to define a set of traits that they think @@ -496,6 +520,11 @@ Hence, the following rules are enforced on trait names: - `default` and `defaults` (in any letter casing combination) are not allowed as trait names to avoid confusion with default traits. +### swift package dump-package + +The `swift package dump-package` command will include information of the trait configuration for the +package and the dependencies it uses in the JSON output. + ## Impact on existing packages There is no impact on existing packages. Any package can start adopting package @@ -504,6 +533,21 @@ the trait is a enabled by default any consumer might have already disabled all default traits; hence, moving API behind a new default trait could potentially break them. +### Impact on other build systems + +The initial impact on other build systems should be minimal. Exisiting packages +must not move exisiting APIs behind a trait. For new APIs that are guarded by a +trait a build system must pass the correct `SWIFT_ACTIVE_COMPILATION_CONDITIONS` +when building the modules of the package. Other build systems might want to +consider to expose a way to model traits in their target description. + +### Impact on documentation + +Traits are used to conditionally compile code. When building documentation the +symbol graph extracter will only see the code that is actually compiled. Systems +that produce documentation for packages should default to building with all +traits enabled so that all API documentation is visible. + ## Future directions ### Consider traits during dependency resolution @@ -529,14 +573,13 @@ Rust's `cfg` macro. Since trait unification is done for every package in the graph during build time the information which module enabled which trait of its dependencies is lost. -Rather the build system start to build from the bottom up while setting all the -compiler defines for the unified traits. As a consequence it might be that a -package accidentally uses an API from a dependency which is guarded by a trait -that another package in the graph has enabled. Since the traits that any one -package in the graph enables on its dependencies are not considered part of the -semantic version, it can happen that disabling a trait could result in breaking a -build. In the future, we could integrate trait checking further into the compiler -where it understands if an API is only available if a certain trait is set. +As a consequence it might be that a package accidentally uses an API from a +dependency which is guarded by a trait that another package in the graph has +enabled. Since the traits that any one package in the graph enables on its +dependencies are not considered part of the semantic version, it can happen that +disabling a trait could result in breaking a build. In the future, we could +integrate trait checking further into the compiler where it understands if an +API is only available if a certain trait is set. > Cargo currently [treats this similar](https://users.rust-lang.org/t/is-disabling-features-of-a-dependency-considered-a-breaking-change/94302/2) @@ -553,6 +596,27 @@ with the future direction "Consider traits during dependency resolution". If default traits depend on the target build platform then this must be an input to the dependency resolution. +### Globally configured traits + +One use-case where mutually exclusive traits are used is to configure the +behaviour of a single package globally. This means that only the final +executable is deciding what trait to enable. In a future proposal, we could +introduce a new setting for marking a trait as globally configured and check +that only executable targets are enabling such a trait. + +### Tooling to test different trait combinations + +Since a single package should support building with any combination of traits, +it would be helpful to offer package authors tooling to build and test all +combinations. A new option `--all-trait-combinations` could be added to +`swift test/build/run` make testing all combinations easy as possible. + +### Surface traits in documentation + +If the compiler gains knowledge about package traits in the future, we could +extract information if a public API is guarded by a trait and surface this in +the documentation. + ## Alternatives considered ### Different naming @@ -570,6 +634,20 @@ A lot of the other considered names have other meanings in the language already. For example `feature` is already used in expressing compiler feature via `enable[Upcoming|Experimental]Feature` and the `hasFeature` check. +### Using SPI instead + +During the investigation how to solve the optional dependency problem `@_spi` +was considered; however, the problem with `@_spi` is that the code is still +compiled and present in the final binary. Optional dependencies can't work like +this since the symbols potentially aren't present during compile time. + +### Enum based traits + +Traits could be expressed via an enum in the package description which would +make sure that they are statically typed. This proposal decided to use `String` +based trait names instead to align with the other definitions inside the package +description such as `targets` or `products`. + ## Prior art Other dependency managers have similar features to control optional dependencies From e0f34fb686b061a1ae4016170dec6b0b5f9688b8 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 1 Jul 2024 11:10:49 +0100 Subject: [PATCH 3/6] Minor fix-ups --- proposals/NNNN-swiftpm-package-traits.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/proposals/NNNN-swiftpm-package-traits.md b/proposals/NNNN-swiftpm-package-traits.md index 893136e328..a7e5705991 100644 --- a/proposals/NNNN-swiftpm-package-traits.md +++ b/proposals/NNNN-swiftpm-package-traits.md @@ -27,7 +27,7 @@ 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 +however, it requires users to discover those adjacent repositories and add additional dependencies to their project. ### Configurable behavior @@ -87,7 +87,7 @@ let package = Package( ) ``` -When depending on a package the default trait is enabled. However, the enabled +When depending on a package the `default` trait is enabled. However, the enabled traits can be customized by passing a set of enabled traits when declaring the dependency. When specifying the enabled traits of the dependencies the `.default` trait can be passed which will enable the default trait. The below @@ -634,6 +634,10 @@ A lot of the other considered names have other meanings in the language already. For example `feature` is already used in expressing compiler feature via `enable[Upcoming|Experimental]Feature` and the `hasFeature` check. +_Package traits_ are also consistent with [the "traits" concept in the +`swift-testing` +library](https://github.com/apple/swift-testing/blob/25d0eed9b339de36365ff16deb9a3d9c64322f1c/Sources/Testing/Traits/Trait.swift#L22). + ### Using SPI instead During the investigation how to solve the optional dependency problem `@_spi` From 962adce64b12b51d4eb670321e7437aa1ced2d55 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Mon, 1 Jul 2024 20:08:08 +0100 Subject: [PATCH 4/6] Update Implementation status --- proposals/NNNN-swiftpm-package-traits.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/proposals/NNNN-swiftpm-package-traits.md b/proposals/NNNN-swiftpm-package-traits.md index a7e5705991..b9bbf539fd 100644 --- a/proposals/NNNN-swiftpm-package-traits.md +++ b/proposals/NNNN-swiftpm-package-traits.md @@ -3,7 +3,14 @@ * Proposal: [SE-NNNN](NNNN-swiftpm-package-traits.md) * Authors: [Franz Busch](https://github.com/FranzBusch), [Max Desiatov](https://github.com/MaxDesiatov) * Review Manager: TBD -* Status: **Work in progress implementation** https://github.com/apple/swift-package-manager/pull/7392 +* Status: **Implemented** + +- https://github.com/swiftlang/swift-package-manager/pull/7704 +- https://github.com/swiftlang/swift-package-manager/pull/7703 +- https://github.com/swiftlang/swift-package-manager/pull/7702 +- https://github.com/swiftlang/swift-package-manager/pull/7701 +- https://github.com/swiftlang/swift-package-manager/pull/7694 +- https://github.com/swiftlang/swift-package-manager/pull/7689 ## Introduction From 51c3118bc24dfb84c9f553d0557d9a7464660f32 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Tue, 2 Jul 2024 13:00:45 +0100 Subject: [PATCH 5/6] Update proposals/NNNN-swiftpm-package-traits.md Co-authored-by: Max Desiatov --- proposals/NNNN-swiftpm-package-traits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/NNNN-swiftpm-package-traits.md b/proposals/NNNN-swiftpm-package-traits.md index b9bbf539fd..8b88460ae8 100644 --- a/proposals/NNNN-swiftpm-package-traits.md +++ b/proposals/NNNN-swiftpm-package-traits.md @@ -3,7 +3,7 @@ * Proposal: [SE-NNNN](NNNN-swiftpm-package-traits.md) * Authors: [Franz Busch](https://github.com/FranzBusch), [Max Desiatov](https://github.com/MaxDesiatov) * Review Manager: TBD -* Status: **Implemented** +* Status: **Experimental implementation available, gated on `@_spi(ExperimentalTraits)` in package manifests and `--experimental` prefix for CLI options** - https://github.com/swiftlang/swift-package-manager/pull/7704 - https://github.com/swiftlang/swift-package-manager/pull/7703 From 3aa2bc4db8a42d560199e47bfcec38c7a23d15d2 Mon Sep 17 00:00:00 2001 From: Mishal Shah Date: Thu, 24 Oct 2024 09:53:22 -0700 Subject: [PATCH 6/6] Start the review for 0450-swiftpm-package-traits.md --- ...-package-traits.md => 0450-swiftpm-package-traits.md} | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) rename proposals/{NNNN-swiftpm-package-traits.md => 0450-swiftpm-package-traits.md} (98%) diff --git a/proposals/NNNN-swiftpm-package-traits.md b/proposals/0450-swiftpm-package-traits.md similarity index 98% rename from proposals/NNNN-swiftpm-package-traits.md rename to proposals/0450-swiftpm-package-traits.md index 8b88460ae8..0a1da605fa 100644 --- a/proposals/NNNN-swiftpm-package-traits.md +++ b/proposals/0450-swiftpm-package-traits.md @@ -1,9 +1,10 @@ # Package traits -* Proposal: [SE-NNNN](NNNN-swiftpm-package-traits.md) +* Proposal: [SE-0450](0450-swiftpm-package-traits.md) * Authors: [Franz Busch](https://github.com/FranzBusch), [Max Desiatov](https://github.com/MaxDesiatov) -* Review Manager: TBD -* Status: **Experimental implementation available, gated on `@_spi(ExperimentalTraits)` in package manifests and `--experimental` prefix for CLI options** +* Review Manager: [Mishal Shah](https://github.com/shahmishal) +* Status: **Active Review (October 24th...November 7th, 2024**) +* Experimental Implementation: Gated on `@_spi(ExperimentalTraits)` in package manifests and `--experimental` prefix for CLI options - https://github.com/swiftlang/swift-package-manager/pull/7704 - https://github.com/swiftlang/swift-package-manager/pull/7703 @@ -669,4 +670,4 @@ and conditional compilation. - [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 +- [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).