3.1.2 用特性提供可配置的包

原文链接: https://docs.swift.org/latest/documentation/packagemanagerdocs/packagetraits/

3.1.2 用特性提供可配置的包

定义一个或多个包特性,为包提供默认功能和可配置功能。

概述

在 6.1 版本之前,Swift 包对每个版本提供的都是一套不可配置的 API。从 Swift 6.1 开始,包可以提供特性(trait),用来表达该包的一套可配置 API。

用特性在包的核心 API 之外启用额外的 API。例如,某个特性可以启用实验性 API、需要额外依赖的可选扩展功能,或者你只希望在特定情况下启用的功能。

你在构建某个包时指定的特性,只在该包内部生效。如果你的包想使用某个依赖包中的特性,就需要在依赖中写明它所需要的特性。

注意:启用某个特性时,不要移除或禁用公共 API。

在定义特性的包内部,该特性表达了条件编译。Swift Package Manager 会把已启用的特性作为条件编译块(例如 #if YourTrait)暴露出来,你可以用它们有条件地启用导入,或在代码中选择不同的编译路径。

特性名称在承载它们的包内是有命名空间的。一个包中的特性名称不会影响任何其他包。特性名称必须是有效的 Swift 标识符,此外还可以包含 - 和 + 这两个字符。不要使用特性名 default 或 defaults(无论大小写)。不允许这些名称,是为了避免与包定义的默认特性混淆。

声明特性

创建一个特性来定义额外的功能,并在包清单的 traits 属性中定义它。使用 .default(enabledTraits:) 提供该包默认使用的那组特性。如果你没有定义默认启用的特性集合,Swift Package Manager 默认不启用任何特性。

下面的例子展示了一个默认启用的单一特性 FeatureA:

1
2
3
4
5
6
// ...
traits: [
    .trait(name: "FeatureA"),
    .default(enabledTraits: ["FeatureA"]),
],
// ...

特性也可以代表一组其他特性,这让你能够把功能分组。下面的例子定义了三个特性,以及一个额外的特性(B-and-C),它会同时启用特性 FeatureB 和 FeatureC:

1
2
3
4
5
6
7
8
9
// ...
traits: [
    .trait(name: "FeatureA"),
    .trait(name: "FeatureB"),
    .trait(name: "FeatureC"),
    .trait(name: "B-and-C", enabledTraits: ["FeatureB", "FeatureC"]),
    .default(enabledTraits: ["FeatureA"]),
],
// ...

对于上面的例子,默认特性是 FeatureA。

注意:如果修改包的默认特性集合会移除 API,那就是一次主版本语义变更。 增加特性不是主版本变更。

Swift Package Manager 把特性视为纯粹可叠加的,并会在整个构建图中的所有包之间统一已启用的特性。请这样设计你的特性:它们启用的是额外的 API(以及必要时的相应依赖)。

定义互斥的特性

包清单格式不支持声明互斥的特性。在少数确实需要提供互斥特性的情况下,请在代码中保护这种场景:

1
2
3
#if FeatureA && FeatureC
#error("FeatureA and FeatureC are mutually exclusive")
#endif // FeatureA && FeatureC

注意:提供互斥特性可能导致开发者在同时启用它们时出现编译错误。

依赖带特性的包

未指定特性的包依赖,会以该包的默认特性启用的方式使用它。要启用特定特性,请把它们加入包依赖声明中的 traits 参数。

下面的例子展示如何依赖 swift-configuration,并同时启用 defaults 和 YAML 两个特性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
dependencies: [
    .package(
        url: "https://github.com/apple/swift-configuration.git",
        from: "1.0.0",
        traits: [
            .defaults,
            "YAML"
        ]
    ),
]

提示:当你为某个依赖指定特性时,你显式定义了要启用哪些特性。 默认特性不会被自动包含进来。 如果要同时使用默认特性和额外特性,请在指定的特性列表中加上 .defaults。

在代码中使用特性

把特性的名称用于条件编译。把该特性对应的额外 API 包在条件编译块里。例如,如果定义了特性 FeatureA 并且它处于启用状态,编译器就会看到并编译函数 additionalAPI():

1
2
3
4
5
#if FeatureA
public func additionalAPI() {
  // ...
}
#endif // FeatureA

用特性启用条件依赖

你可以用特性来可选地引入某个依赖,或者引入启用了特定特性的依赖,以支撑你通过该特性公开的功能。为此,先把需要的依赖加入清单的 dependencies 声明,然后为包中定义的某个或某些特性使用条件依赖,把该依赖添加到某个目标上。

下面的例子展示了一个包清单的相关部分,它定义了特性 FeatureB,以及一个仅在该特性启用时才使用的本地依赖:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ...
traits: [
    "FeatureB"
    // 这个特性只存在于*这个*包内
],
dependencies: [
    .package( 
        path: "../some/local/path",
        traits: ["DependencyFeatureTrait"] 
    // 这会在 ../some/local/path 处的本地依赖上
    // 启用特性 DependencyFeatureTrait。
    )
]
// ... 
targets: [
    .target(
        name: "MyTarget",
        dependencies: [
            .product(
                name: "MyAPI",
                package: "MyDependency",
                condition: .when(traits: ["FeatureB"]) 
    // 如果在*这个*包中启用了 FeatureB 特性,那么
    // 产品 `MyAPI` 就会作为 `MyTarget` 的依赖被包含进来。
            )
        ]
    ),
]

下面的代码把导入包在特性的条件编译中,并定义了使用该依赖的额外 API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#if FeatureB
    import MyAPI
#endif // FeatureB

// ...

#if FeatureB
    public func additionalAPI() {
        MyAPI.provideExtraFunctionality()
        // ...
    }
#endif // FeatureB