SwiftPM Package Traits 终极指南:为 Swift 包设计可配置 API
SwiftPM Package Traits 终极指南为 Swift 包设计可配置 API【免费下载链接】swift-package-managerThe Package Manager for the Swift Programming Language项目地址: https://gitcode.com/gh_mirrors/sw/swift-package-managerSwift 6.1 起SwiftPM 的 Package Traits 让一个包能同时提供默认 API 与按需追加的可选 API。本文面向有开发经验、但没接触过 traits 的 Swift 开发者讲清 trait 声明语法、命名规则、依赖方启用上游 trait 的方式、--traits 等命令行开关与 EnabledTraitsMap 的合并逻辑附避坑清单。你为什么会遇到包太重的问题包的公开 API 在发版那一刻就冻结了Swift 6.1 之前作者写的功能要么恒在、要么恒缺使用者没有选择权。三类场景下你会明显感到这种束缚。你的库里有个实验性功能只有少数用户需要。放进主 target所有用户都替它付编译时间和二进制体积。某个扩展能力需要额外依赖比如某个第三方解析器。无条件声明依赖所有人的构建图都会跟着膨胀。反过来当你站在依赖方你只用了某个大库的核心部分却关掉不了它默认开着的可选项。traits 就是 SwiftPM 为此加的机制库作者在 manifest 里声明一组开关使用方按需挑选要打开哪些SwiftPM 负责把开关在整个构建图里传递、合并。5 分钟跑通一个 trait最小心智模型一个 trait manifest 里的一条声明 源码里一段#if条件编译块。下面是最小 manifest声明一个默认启用的 traitFeatureA// swift-tools-version:6.1 import PackageDescription let package Package( name: MyLib, products: [.library(name: MyLib, targets: [MyLib])], targets: [.target(name: MyLib)], traits: [ .trait(name: FeatureA, description: Enables the JSON export API), .default(enabledTraits: [FeatureA]), ] )源码侧直接拿 trait 名当条件编译块名启用时这段代码才进入编译public struct MyLib { public init() {} #if FeatureA public func exportAsJSON() - String { ... } #endif // FeatureA }三条命令验证一下开关行为swift build # 默认集合生效FeatureA 开启 swift build --disable-default-traits # 默认关闭#if FeatureA 块整体被排除 swift build --enable-all-traits # 全部 trait 开启这里为什么这么写trait 名即宏名没有别的运行时钩子条件块之外的代码对开关完全无感。往下深入之前先记住两条语义——纯增量启用 trait 绝不能移除或禁用已有公开 API只能追加。SwiftPM 在整个构建图内对 traits 做并集合并纯增量才能保证任何组合都不破坏别人的代码路径。作用域仅限当前构建你指定的 traits 只对正在编译的这个包生效像一张只对本次演出有效的票。想给某个依赖开它的 trait必须写进对它的依赖声明见下一节。声明 trait 的完整参考 声明 trait 的三种写法Package的traits属性接收SetTrait每个Trait有三个字段参数类型默认值说明nameString必填trait 的规范名用于启用它、在 manifest 其他位置引用它同时也是条件编译块的名字descriptionString?nil解释该 trait 启用的额外功能enabledTraitsSetString[]启用本 trait 时顺带启用的本包其他 trait三种写法可以混用在同一个数组里traits: [ .trait(name: YAML), // 显式构造 JSON, // 字符串字面量简写 .default(enabledTraits: [YAML]), // 默认启用集合 ],因为Trait遵循ExpressibleByStringLiteral字符串字面量就是只带名字的形式。.default(enabledTraits:)本质上是一个特殊 trait名字固定为defaultdescription 固定为 The default traits of this package.见 Trait.swift 第 60-66 行。不写.default时SwiftPM 默认不启用该包的任何 trait。命名规则定义在 Trait.swift 的文档注释中要点有四合法 Swift 标识符之外额外允许-和首字符须为 Unicode XID 起始字符大部分字母、数字或_后续字符须为 XID 延续字符数字、_、大部分字母、-或。所以B-and-C这类组合名合法default和defaults任意大小写组合被禁止用作 trait 名避免和默认集合混淆trait 名在承载它的包内部命名空间隔离两个包可以各定义一个叫Debug的 trait互不干扰manifest 解析后每个 trait 被建模为TraitDescription见 TraitDescription.swift同样持有上述三个字段。组合 trait 与默认集合enabledTraits允许一个 trait 充当套餐启用它等于同时启用被它引用的 traittraits: [ .trait(name: FeatureB), .trait(name: FeatureC), .trait(name: B-and-C, enabledTraits: [FeatureB, FeatureC]), .default(enabledTraits: [FeatureA]), ],维护默认集合时请记住版本红线收缩默认集合并因此移除 API属于 major 版本变更只是新增 trait则不构成。⚠️ 注意不要用开启 A 就关闭 B的方式做互斥——那等于移除 API直接违反纯增量原则。互斥保护#errormanifest 格式没有互斥 trait的声明能力。确实需要互斥时只能在代码层兜底#if FeatureA FeatureC #error(FeatureA and FeatureC are mutually exclusive) #endif // FeatureA FeatureC用户同时启用两者会得到一条编译错误明确告知原因——这正是期望的行为。依赖方如何启用上游 trait核心一句话在依赖声明里给出traits列表就是显式枚举默认 traits 不会自动包含。dependencies: [ .package( url: https://example.com/org/swift-configuration.git, from: 1.0.0, traits: [ .defaults, // 要默认功能必须自己加 YAML, ] ), ],想要默认功能 YAML就两个都写只写traits: [YAML]会让依赖以仅 YAML 的状态构建。底层模型是 PackageDependencyDescription.swift 里的嵌套类型PackageDependency.Traitname加可选conditioncondition表达本包启用某 trait 时才把依赖的这个 trait 也启用文件系统、源码控制、注册表三种依赖形态都持有traits: SetTrait?字段nil即未指定依赖按自己的默认 traits 构建。条件依赖把 product 绑到 trait 上trait 暴露的功能如果自身还要拉额外依赖就把依赖挂上条件让它只在 trait 启用时进入构建图targets: [ .target( name: MyTarget, dependencies: [ .product( name: MyAPI, package: MyDependency, condition: .when(traits: [FeatureB]), ), ] ), ],源码侧对应的 import 放进同一个条件块两侧同步出现、同步消失#if FeatureB import MyAPI #endif // FeatureB这么写的好处没启用 FeatureB 的用户不会为 MyAPI 多付一行编译开销启用 FeatureB 的用户依赖与代码同时就位。 提示依赖声明里写traits: []不是没指定而是显式禁用该依赖的默认 traits一次刻意为之的覆盖。命令行开关速查--traits 参数与 TraitConfiguration ⚙️三个开关是swift build、swift test、swift run等命令的全局选项且只作用于根包当前正在构建的包选项类型行为--traits T1,T2,…值选项启用列出的 trait逗号分隔显式枚举时想要默认集合要自己传defaults--enable-all-traits布尔开关启用包的全部 trait--disable-default-traits布尔开关禁用包的全部默认 trait组合行为转换逻辑在 Options.swift 第 879-891 行--enable-all-traits优先级最高一旦置位另外两个被忽略--disable-default-traits--traits A,B只启用 A、B默认关闭单独--disable-default-traits什么都不启用。三个开关最终收敛为TraitConfiguration枚举见 TraitConfiguration.swift的四种状态public enum TraitConfiguration: Codable, Hashable { case enableAllTraits // 来自 --enable-all-traits case disableAllTraits // --disable-default-traits 且未列 trait case enabledTraits(SetString) // 来自 --traits A,B case default // 未传任何参数用包声明的默认 }顺手记一个检查技巧swift package dump-package在未指定 trait 配置时会以启用全部 traits的视角输出包描述DumpCommands.swift 第 61 行跑一次就能看到声明出的所有 trait。构建图里 trait 怎么流动 源码走读压缩成一页入口、沿边传播、三条合并规则。入口与沿边传播updateEnabledTraitsWorkspaceTraits.swift 第 31-67 行先给每个包定起始集合根包直接用命令行转换出的traitConfiguration依赖包查enabledTraitsMap里各父包登记的记录。随后做一次默认合并——只要任一父包请求了默认显式.defaults或压根没指定就把该包 manifest 声明的默认集合并集进来。处理完当前 manifest 后遍历其dependencies对每条依赖边调用updateEnabledTraits(forDependency:parent:)第 74-99 行父包显式指定了 traits就先经isEnabled(by:)过滤掉条件未命中的项再以内置 setter.package(parent)登记剩余项未指定则登记一条该父包希望依赖使用默认的记录。启用意图就这样沿边向下流图中每个包最终拿到所有父请求的并集。EnabledTraitsMap 的三条合并规则数据结构是 EnabledTrait.swift 中的EnabledTraitsMap文件头注释第 18-79 行定义了三条规则Union对同一包的多次写入是合并而非覆盖——像多位家长分别给孩子报兴趣班最终课程表是所有报名的并集Disablers空集合会被单独登记为禁用者与启用者的记录共存而不是互相覆盖Default Setters请求默认的父包单独跟踪同一包既有禁用者又有默认请求者、且没有显式命名 trait 时默认请求者获胜。最终解析在resolvedExplicitTraits第 144-155 行显式命名的 trait 永远优先且已并入默认请求的并集→ 有默认请求者则落到包自身默认 → 只有禁用者返回空集 → 无任何记录返回nil即用依赖自己的默认。内部按包版本分versionedTraits/unversionedTraits存放源码控制依赖有版本、文件系统依赖无版本外层用ThreadSafeBox保证多父包并发登记边时线程安全。还有一条兜底manifest 未声明任何 trait 的旧包supportsTraits false见 ManifestTraits.swift无论父包请求什么一律以 defaults 构建EnabledTrait.swift 第 167-192 行——这正是traits 纯增量、不影响旧版本兼容承诺的落点。测试与样例同样成体系端到端在 Tests/FunctionalTests/TraitTests.swift如tests_whenNoFlagPassed、tests_whenAllTraitsEnabled_andDefaultTraitsDisabledmanifest 解析在 TraitLoadingTests.swift四状态转换在 TraitConfigurationTests.swiftFixtures/Traits/ 下还有 Example、Package1 到 Package11、PackageConditionalDeps、DisablingEmptyDefaultsExample 等多组 fixture可直接当阅读样例。避坑清单只加不减。trait 只追加 API 与依赖。反例用#if !StrictMode藏掉一个弃用方法——用户开 StrictMode 的瞬间编译断裂。默认集合不要随手缩。收缩并移除 API 就升 major 版本只新增 trait 是安全的。依赖方显式。启用上游额外 trait 时把.defaults和目标 trait 一起列。反例traits: [YAML]默默丢掉默认功能。依赖绑到 trait 上。trait 专属依赖走condition: .when(traits:)别做无条件依赖否则并集机制会替用户拖来不需要的包。互斥用#error兜底。manifest 没有互斥声明写进代码让用户同时启用时得到明确的报错而不是诡异的符号冲突。命名别踩线。不用default/defaults组合 trait 起个一眼可读的名字如B-and-C。把 trait 声明好、在 README 里写清启用方式剩下的交给构建图的并集机制——无论几个父包各要各的还是某个旧版依赖根本不支持 traits最终构建图的形态都是确定且可预测的。【免费下载链接】swift-package-managerThe Package Manager for the Swift Programming Language项目地址: https://gitcode.com/gh_mirrors/sw/swift-package-manager创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考