Swift Package Manager 工程手册:从 Package.swift 到跨平台可重建交付
一个 Swift 项目在开发机上能被 Xcode 打开,并不能证明它能在 Linux runner、另一版 Xcode 或干净缓存中重建。真正决定结果的,是 Swift 工具链、Package.swift 所声明的构建图、依赖来源、解析结果、目标平台以及构建插件共同形成的输入集合。
Swift Package Manager,简称 SwiftPM,不只是“下载 Swift 库”的工具。它随 Swift 工具链提供,同时负责包清单解释、依赖解析、模块构建、测试、可执行程序运行,以及部分二进制制品和插件的接入。架构师需要治理的是这条完整链路,而不是某一个 Xcode 菜单。
Package.swift 能声明什么,Package.resolved 能锁住什么
SwiftPM 工程首先要分清两类证据:Package.swift 描述 package、product、target、dependency 和平台约束,Package.resolved 记录一次依赖解析选中的版本或修订。二者共同约束依赖图,却不能单独证明 Xcode、Linux runner、二进制制品和构建插件在目标环境中都可用。真正可交付的结果还取决于 Swift 工具链、SDK、CPU 架构、依赖来源、缓存状态以及构建期间会执行的代码。
因此,排查和治理应沿构建输入继续向外追:源码依赖要检查 SCM 或 Registry,binaryTarget 与 artifact bundle 要检查平台、校验和与下载链路,插件和宏包要按构建期代码执行管理。Swift 语言、界面实现、App Store 发布流程和私有 Registry 服务端运维分别由语言、应用交付与制品平台专题负责;进入 SwiftPM 依赖图之后的解析、构建和供应链影响,则必须在这里形成可验证证据。
先锁定要交付的平台与工具链
先确认项目最终要在哪些环境生成和运行产物:
纯 Swift 库或命令行程序可以使用 Swift.org 工具链,在受支持的 macOS、Linux 或 Windows 上工作。Apple 平台应用通常由 Xcode 提供 Swift 工具链、SDK 和签名环境;提交 App Store 时应遵守 Apple 对 Xcode 工具链的要求。Linux 构建需要与发行版匹配的官方工具链及其系统依赖;引用 Apple 专属框架的 target 不能因为“SwiftPM 支持 Linux”就自动跨平台。
Windows 还需要官方安装说明列出的 Windows SDK 和 C++ 构建组件。
不要在团队规范里只写“使用最新 Swift”。仓库应明确批准的 Swift 或 Xcode 基线、目标 OS、CPU 架构和升级窗口。执行下面命令确认真实入口:
swift --version
swift package --help预期能看到工具链版本和 package 子命令。若 Xcode 与独立工具链并存,macOS 还应记录 xcrun --find swift 与 xcode-select -p,否则终端和 IDE 可能调用不同编译器。
如果当前机器没有 swift 命令,先完成工具链安装再继续;不要拿另一台机器的输出代替目标平台验证。团队落地时应在批准的 OS、Swift/Xcode 和 CPU 矩阵中逐项复验。
安装与工具链入口
优先使用 Swift.org 安装页 对应平台的入口,不从来源不明的镜像复制安装脚本。macOS 开发 Apple 平台时通常安装批准版本的 Xcode;需要独立工具链时,再按 Swift.org 的签名包说明安装。Linux 可使用官方发行包或官方容器镜像,Windows 可按官方说明通过 WinGet 安装平台依赖与 Swift toolchain。
安装后不要只看命令存在,还要确认同一会话中的编译器、包管理器和目标信息来自同一工具链:
which swift
swift --version
swift -print-target-info在 PowerShell 中用 Get-Command swift 替代 which。-print-target-info 能暴露目标三元组、运行库搜索路径和 SDK 信息;这比“我的机器能编译”更适合作为 CI 对照证据。
第一个包:先看懂四类对象
创建最小可执行包:
mkdir ArchitectureProbe
cd ArchitectureProbe
swift package init --name ArchitectureProbe --type executable
swift run ArchitectureProbe
swift testswift run 预期编译并输出模板程序。模板可能没有测试,因此 swift test 的输出依工具链模板而异;团队应新增一个真实测试,而不是把“没有测试”误当成测试通过。
SwiftPM 的核心对象可以沿着构建方向理解:
package 是清单描述的发布与构建边界。target 是编译成模块、测试模块、插件或二进制引用的基本单元。product 是暴露给外部消费者的 library 或 executable,由一个或多个 target 组成。
dependency 是外部 package;本包 target 依赖的是外部 package 暴露的 product,而不是随意依赖其源码目录。
一个可读的清单如下:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "ArchitectureProbe",
platforms: [
.macOS(.v13)
],
products: [
.library(name: "ProbeCore", targets: ["ProbeCore"]),
.executable(name: "probe", targets: ["ProbeCLI"])
],
dependencies: [],
targets: [
.target(name: "ProbeCore"),
.executableTarget(name: "ProbeCLI", dependencies: ["ProbeCore"]),
.testTarget(name: "ProbeCoreTests", dependencies: ["ProbeCore"])
]
)第一行不是注释装饰。Package 的官方定义说明,swift-tools-version 决定可使用的 PackageDescription API、处理清单所需的最低 Swift 工具版本,并影响语言兼容口径。把它升级后,旧 runner 可能在真正编译源码前就无法读取 manifest,所以它应与 CI 工具链变更一起评审和回滚。
依赖声明不是锁定结果
SwiftPM 支持版本范围、精确版本、branch、revision 和本地路径依赖。它们表达的稳定性完全不同:
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
.package(url: "https://code.example.invalid/team/utility.git", exact: "2.4.1"),
.package(url: "https://code.example.invalid/team/experiment.git", branch: "main"),
.package(url: "https://code.example.invalid/team/hotfix.git", revision: "0123456789abcdef")
]示例域名是不可访问占位符。生产选择时要记住:
from 通常表达向下一个主版本之前兼容,但“语义版本正确”仍依赖包作者守约。exact 收紧了升级范围,也把安全修复升级变成人工动作。branch 会随远端移动,适合短期集成,不适合长期可重建基线。
revision 比 branch 稳定,但缺少发布语义和正常升级路径,适合有退出日期的临时修复。
本地依赖用于同机联调:
.package(path: "../Utility")它不会被当作可发布的远程依赖。提交前必须在没有相邻目录的干净 clone 中构建,否则团队会交付一个只在作者目录布局中成立的包。
Package.resolved 的正确边界
执行以下命令显式解析和查看依赖:
swift package resolve
swift package show-dependencies
swift package updateresolve 根据 Package.swift 与已有 Package.resolved 选择可用版本;update 主动寻找约束内的新版本并更新解析结果。两者不能在 CI 中混用:CI 应恢复评审过的输入,而不是在每次构建时顺便升级。
SwiftPM 依赖解析说明明确了 Package.resolved 的作用:它记录顶层包的解析结果。对应用、命令行程序等叶子项目,通常应提交它,并在变更审查中同时看 manifest 与 resolved diff。一个库自身提交的 Package.resolved 不会替调用方锁住依赖;当该库作为另一个包的 dependency 时,它自己的 resolved 文件会被忽略。
需要严格按已解析版本工作时,使用当前工具链帮助中确认参数,再采用官方提供的强制 resolved 版本模式:
swift package resolve --force-resolved-versions不要靠手改 Package.resolved 解决冲突。先修改依赖约束、运行解析、执行测试,再审查新图;手改可能制造格式正确但语义未经解析器验证的证据。
Registry 与源码依赖是两套来源模型
SwiftPM 可以从 Git 源码地址获取包,也可以使用实现 Swift Package Registry 规范的 Registry。Registry 使用说明给出了项目级与用户级配置边界:项目级配置保存在 .swiftpm/configuration/registries.json,用户级配置位于用户 SwiftPM 配置目录。团队要先决定哪些 registry 映射属于仓库契约,哪些只属于个人凭证。
设置项目默认 Registry,并使用 scope 标识声明依赖:
swift package-registry set https://packages.example.invaliddependencies: [
.package(id: "example.utility", .upToNextMajor(from: "1.0.0"))
]Registry 与 SCM 来源之间还存在“保持原来源、用 registry identity 去重、尽量用 registry 替换 SCM”等策略。不要在升级工具链时悄悄改变策略,因为它会改变下载来源、认证路径、审计证据和失败回退。
需要认证时优先交互登录或安全注入:
swift package-registry login https://packages.example.invalid --username build-user官方行为是优先保存到操作系统凭证存储;不支持安全存储的平台可能回退到 ~/.netrc。CI 不应把 token 写入命令历史、manifest、Registry 配置或构建日志;若必须使用 netrc,应限制文件权限、使用短期凭证并在任务结束后销毁工作区。
二进制依赖与 artifact bundle
远程二进制 target 需要 URL 与校验和:
.binaryTarget(
name: "TelemetryBinary",
url: "https://downloads.example.invalid/TelemetryBinary-2.1.0.zip",
checksum: "<sha256-from-approved-artifact>"
)校验和必须由实际归档计算:
swift package compute-checksum TelemetryBinary-2.1.0.zip不要复制供应方网页上来源不明的字符串,更不能为了升级“先改 URL,校验失败再随便改 checksum”。URL、版本、checksum、许可证、SBOM 或来源审批应作为一个变更单元。二进制 target 还受 Swift/ABI、目标 OS、CPU 架构和最低平台版本约束;下载成功不代表可链接。
Artifact bundle 可以携带按平台选择的可执行工具或制品元数据,常用于插件所需工具。它解决分发结构问题,不消除第三方二进制执行风险。团队要验证 bundle 支持的 triplet、签名或校验、许可证和离线镜像策略。binary target 的 checksum 说明可用于复核远程归档的校验入口。
插件与宏:依赖恢复开始执行代码
build tool plugin 会进入构建流程,command plugin 可由开发者显式调用。插件可能读取包目录、运行随包分发的工具并生成源码或资源。宏的实现也在编译期参与构建。它们带来的风险不是普通源码依赖可完全覆盖的:
插件或宏升级可能在 CI 身份下执行新逻辑。网络隔离、只读工作区和沙箱能力因平台与工具链而异。生成文件若未稳定排序或嵌入本地路径,会破坏可重建性。
二进制工具可能只支持部分 OS/CPU,使 Xcode 成功而 Linux CI 失败。
因此新增插件、宏或其传递依赖时,应把执行权限、输入输出目录、网络需求、二进制来源和回滚方式列入评审。CI 中保留插件执行日志,但在归档前清理路径、用户名和凭证。
构建、测试、缓存与清理
常用入口应由仓库脚本封装,但底层命令要让排障者看得见:
swift package resolve
swift build
swift test
swift run probe
swift package describe
swift package show-dependencies
swift package clean
swift package resetclean 面向构建产物;reset 影响更广,会丢弃已解析依赖和构建状态。执行前先看当前工具链帮助并确认影响,不要把用户级缓存目录当普通临时目录递归删除。共享 runner 清缓存还可能影响并发任务。
缓存只能加速,不能成为唯一依赖来源。合格的 CI 至少要周期性在空缓存中完成解析、构建和测试,并记录:
Swift/Xcode 版本与目标信息;Package.swift、叶子项目 Package.resolved 的摘要;Registry/SCM 路由策略,不含凭证;
OS、CPU、SDK 和构建 configuration;测试结果与可交付产物摘要。
缓存键至少包含工具链、目标平台、架构和 resolved 输入。只按分支名缓存,会让一次错误解析长期污染后续构建。
Xcode 集成与 CLI 为什么会不一致
Xcode 能直接添加 Swift package,但它还带入 workspace、scheme、configuration、Apple SDK、DerivedData 和账号状态。CLI 从包根目录执行时,则主要读取 Package.swift 和 SwiftPM 配置。两边出现差异时,按输入层排查:
比较 Xcode 选中的 toolchain 与 swift --version。确认 Xcode 工程中的 Package.resolved 实际位置与仓库提交策略。比较 scheme/configuration、目标平台、环境变量和编译条件。
清理前先保留失败日志,再分别验证 SwiftPM 构建目录与 DerivedData。在干净 clone 中用仓库标准 CLI 命令复现,排除 IDE 用户态设置。
团队不能把“点击 Xcode 的 Resolve Package Versions”作为唯一恢复入口。仓库必须有无界面的 CI 命令,同时承认 Apple 应用最终仍需在批准 Xcode/SDK 组合上验证。
跨平台与 CI 契约
跨平台不是同一份源码在三个 runner 上执行 swift build 就结束。首先把 target 分成纯 Swift、系统库、C/C++ 互操作、Apple framework、二进制 target、插件/宏六类,再逐类确认平台能力。
一个可执行的矩阵通常至少包含:
macOS + approved Xcode + arm64
Linux + approved Swift toolchain + x86_64
Windows + approved Swift toolchain + required Windows SDK并非每个项目都要覆盖三者;只声明真实支持的平台。矩阵中的每格都应执行 resolve、build、test,并在涉及二进制或插件时验证其目标架构。平台专属代码用清晰 target 边界或条件编译隔离,不要等到链接阶段才发现依赖不可用。
常见失败:按证据向上追
manifest 无法加载
报 tools version 不支持、PackageDescription API 不存在或 manifest 编译失败。
先执行 swift --version,再看清单第一行和失败位置。
runner 工具链低于 swift-tools-version,或终端/Xcode 使用了不同工具链。
修复与再验证:切换批准工具链,或在确认未使用新 API 后回退 tools version;重新执行 swift package describe 与 swift build。
依赖解析冲突或意外升级
没有可满足版本、resolved 文件大幅变化,或新 clone 选择了不同版本。
审查 Package.swift 约束、Package.resolved diff 和 swift package show-dependencies。
约束交集为空、CI 执行了 update、叶子项目未提交 resolved,或 branch/revision 被误当稳定版本。
修复与再验证:收敛约束并明确升级目标,重新 resolve、build、test;不要直接删除 resolved 后把新图无审查提交。
Git、Registry、代理或 CA 失败
超时、TLS 校验失败、401/403,或公开依赖成功而私有依赖失败。
区分 SCM 与 Registry 请求,检查实际 URL、代理环境、企业 CA、Registry 映射和凭证存储。
只给 Git 配了代理、CI 未安装 CA、token scope 不足,或本地配置覆盖了用户配置。
修复与再验证:在最小权限下修复对应层;禁止关闭 TLS 校验。用不输出 secret 的登录/resolve 验证,再从空工作区复验。
checksum、指纹或二进制链接失败
binary checksum 不匹配、Registry fingerprint 冲突、找不到目标架构 slice 或链接符号。
分别核对归档字节、官方计算 checksum、Registry 来源、OS/CPU/最低平台和 ABI 条件。
同 URL 内容被替换、镜像与上游字节不同、制品不支持当前 target,或缓存保留旧内容。
修复与再验证:停止使用可变制品,恢复已批准 URL+checksum 组合;清理项目级状态后在空缓存验证。不要把严格指纹检查降级当常规修复。
插件在本机成功、CI 失败
生成文件缺失、权限拒绝、工具不可执行或输出不稳定。
确认插件类型、输入输出目录、实际可执行文件、目标平台和 CI 沙箱/权限。
依赖本机绝对路径、未声明工具、需要网络、二进制不跨平台或生成过程不确定。
修复与再验证:把输入、工具和输出声明收敛到包契约;在只读、断网或空缓存场景分别验证;不能收敛时移出构建关键路径。
升级、迁移与回滚
SwiftPM 升级通常与 Swift 或 Xcode 升级绑定。不要只改 CI 镜像标签。一次稳健升级包括:
建立旧工具链基线,保存 resolve/build/test 与产物摘要。在分支上升级 toolchain,暂不提高 swift-tools-version,先识别编译器和解析行为差异。需要新 manifest API 时再提高 tools version,并复验所有 runner。
独立审查 Package.resolved、插件、宏、binary target 与 Registry 路由变化。灰度合并后保留旧镜像、旧 Xcode 和成对回退 manifest/resolved 的能力。
回滚不是只恢复 Package.swift。若升级同时改变了工具链、Registry 配置、resolved 文件或二进制 checksum,这些输入必须按同一变更单回退。
可重建与安全有时会冲突
把依赖永久固定在旧 revision 能提高短期重复性,却可能滞留漏洞;始终追最新则把未经审查的代码带入构建。实践中应将日常 CI 设为只读恢复,另设升级任务提出 manifest/resolved diff、安全信息和回滚点,由 owner 审批。
用户态配置会制造“隐形成功”
开发者 Keychain、~/.netrc、用户级 Registry 映射、Git rewrite 和本机缓存都不会随仓库交付。判断标准不是老机器构建成功,而是新身份、干净 home、空缓存仍能按文档恢复。失败时先列出配置层,不要要求开发者上传包含凭证的完整 home 目录。
插件把供应链风险前移到编译期
普通源码最终会被编译,插件和宏实现则可能在构建过程中执行。团队应为可执行构建依赖设置 owner、版本审批、权限边界和停用开关。无法解释插件访问了什么、生成了什么、如何停用,就不应进入关键交付链。
Apple 与非 Apple 平台不能假装同构
Xcode 提供 Apple SDK、签名和特定构建行为;Swift.org 工具链强调语言与跨平台能力。共用 Package.swift 不代表运行环境相同。架构决策应把平台专属 target 显式隔离,并用真实矩阵证明支持范围。
团队治理清单
仓库声明批准的 Swift/Xcode、OS、CPU 和 SDK 基线。swift-tools-version 变更与工具链升级一起评审。package、product、target 与外部 dependency 边界清楚。
叶子项目明确 Package.resolved 提交策略,CI 不执行隐式升级。branch、revision 和本地 path 依赖都有 owner、期限和退出条件。Registry/SCM 路由、代理与 CA 可在干净身份中重建。
凭证进入安全存储,日志、manifest 和 URL 不含 token。binary target 的来源、checksum、平台、架构和许可证已审查。插件、宏和 artifact bundle 进入可执行供应链审批。
空缓存构建与跨平台矩阵定期运行。Xcode 与 CLI 差异有固定排查顺序和证据保留规则。升级能成对回退工具链、manifest、resolved、Registry 与二进制输入。
