包的依赖管理
本教程共 93 篇 · 第 89 篇 · 更新于 2026-08-08 · 约 7 分钟阅读
本节目标:能在 Package.swift 里添加外部依赖,说清各种版本区间写法的区别,并把依赖正确链接到自己的 target。
上一章我们把包搭起来了,但 dependencies 还是空的。真实开发里,你几乎一定会用到别人写好的包。这一章就讲怎么声明依赖、版本号怎么写、以及 target 怎么”接上”这些依赖。
1-1 依赖长什么样
依赖写在 Package.swift 顶层 Package 的 dependencies 数组里。每一项用 .package(...) 描述从哪来、要哪个版本。
// swift-tools-version:6.0
import PackageDescription
let package = Package(
name: "hello-pkg",
products: [
.executable(name: "hello-pkg", targets: ["hello-pkg"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
],
targets: [
.executableTarget(
name: "hello-pkg",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
)
]
)
注意一个容易混淆的点:顶层 dependencies 说的是”我要用这个包”,而 target 自己的 dependencies 说的是”这个 target 具体用到包的哪个 product”。两处都得写,缺一不可。
1-2 用 URL 指定来源
.package(url:from:) 里的 url 是包仓库地址。最常见的是 GitHub 的 https 链接。SPM 支持 Git 仓库,所以只要是能被 git clone 的地址都行,包括公司内网的私有仓库。
NoteSPM 拉取的是 Git 仓库的某个版本(tag 或分支)。它不会去下载 zip 包,而是走 Git 协议,所以本地通常需要配置好 git 凭据。
1-3 版本区间:from 的语义
from: "1.2.0" 是最常用的写法。它的意思是”至少 1.2.0,但只升到下一个大版本之前”。具体说,它允许 1.2.0、1.3.0、1.9.9……但不允许 2.0.0。
这套规则叫”向上兼容到下一个 major”。因为语义化版本(SemVer)约定:大版本号变了就意味着可能不兼容。所以 from 默认停在 1.x 内,既让你拿到新功能,又避免被破坏性更新坑到。
1-4 更精细的版本控制
除了 from,还有几种写法,按需要挑。
// 精确到某个版本
.package(url: "...", exact: "1.2.3")
// 指定一个闭合/半开区间
.package(url: "...", "1.2.0"..<"2.0.0")
.package(url: "...", "1.2.0"..."1.5.0")
// 跟随某个分支(不推荐用于正式发布)
.package(url: "...", branch: "main")
// 锁定到某次提交
.package(url: "...", revision: "a1b2c3d")
exact 最死板,只有这一个版本能用,适合你对兼容性极度敏感时。..< 和 ... 是标准区间写法,左闭右开和闭合区间,你能精确框死范围。branch 和 revision 适合依赖还在疯狂迭代、没发正式版本的情况,但正式项目里尽量少用,因为它不稳定、别人也难复现。
Warning用
branch: "main"看似省事,可 main 随时在变,今天能编明天可能就挂。正式依赖优先用带 tag 的版本区间。
1-5 依赖冲突与版本解析
当你依赖 A、A 又依赖 B,而你直接也依赖 B,SPM 会做”版本解析”:找出一个能让所有要求都满足的 B 版本。如果找不到,就会报冲突错误。
解析结果会被记进 Package.resolved 文件。它锁定了每个依赖实际用到的版本,保证你和队友、和你三个月后跑出来的结果一致。
$ swift package resolve # 按 Package.swift 重新解析并下载
$ swift package update # 在允许区间内升级到最新
Tip把
Package.resolved提交进版本控制(git)。它能让团队所有人、CI 机器拿到一模一样的依赖版本,避免”在我电脑上能跑”的尴尬。
1-6 在 target 里链接 product
顶层声明了依赖,还得在 target 里”点名”要用哪个 product。看回开头的例子:
.executableTarget(
name: "hello-pkg",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
)
name 是那个包对外暴露的 product 名(通常和库名一致),package 是顶层 .package 里写的包名(仓库对应的名字)。两者不是一回事,初学者常写反。
一旦链接好,源码里就能 import ArgumentParser 了。
1-7 依赖一个本地包
除了远程仓库,你也能依赖本机另一个目录里的包,常用于把大项目拆成多个子包、在本地联调。
.package(path: "../MyLocalLib")
path 指向本地文件夹。注意它不走版本区间——本地包就是当前磁盘上的样子。等子包发布到远程后,再把 path 换成 url 即可。
1-8 平台与 Swift 版本限制
有时某个依赖只在特定平台或 Swift 版本可用。你可以在清单里加条件,让解析更聪明。
.product(
name: "ArgumentParser",
package: "swift-argument-parser",
condition: .when(platforms: [.macOS])
)
这表示这个 product 仅在 macOS 平台链接。SPM 会据此跳过不兼容平台的依赖,避免构建报错。
1-9 语义化版本到底怎么读
理解依赖区间前,得先懂版本号 主.次.补(如 1.2.3)的含义。第一位大版本变了,意味着可能不兼容旧用法;第二位小版本是向后兼容的新功能;第三位补丁只修 bug、绝不改行为。
SPM 的 from 规则正是建立在这套约定上:它敢自动升到 1.9.9,是因为按约定小版本不会破坏你;它停在 2.0.0 之前,是因为大版本可能不兼容。你发布自己的包时,也该遵守这套约定,别在补丁版本里偷偷删 API。
1-10 传递依赖与版本打架
你的包依赖 A,A 又依赖 B,B 还依赖 C——这一串叫传递依赖。你通常只在顶层写 A,B/C 由 SPM 顺着 A 的清单自动拉。你不用(也不该)手动把每一层都写进自己的 dependencies。
麻烦出在版本冲突:假设你的包要 B 的 1.0.0,而 A 要 B 的 2.0.0,且这俩不兼容,SPM 解析失败,报 “conflicting dependencies”。解法通常是:升级你这边的要求到兼容版本,或者联系 A 的作者放宽它对 B 的限制。这种”依赖地狱”在大型项目里常见,所以尽量别把版本写死成 exact。
1-11 Package.resolved 长什么样
resolve 之后生成的 Package.resolved 里,记录了每个依赖最终锁定的 commit 或版本号。它本质是个 JSON,列出仓库地址、解析到的版本、对应的 Git 哈希。
提交它进 git 的意义在于可复现:队友 git pull 后 swift build,拿到的依赖版本和你完全一致,不会因为”上游刚好发了新版本”而表现不同。CI 机器同理。想故意升级,再跑 swift package update,它会按区间挑最新并刷新 resolved 文件。
1-12 选版本的实用建议
给正式项目加依赖,优先 .package(url:from:) 让补丁和小版本自动进。只有当你明确知道某 API 在 1.3 才出现、又在 2.0 被改,才用 ..< 收窄到 1.3.0..<2.0.0。exact 留给那些你对兼容性零容忍、且上游确实稳定的极少数场景。branch 和 revision 当作应急通道,进了正式代码就尽快换成带 tag 的版本。
1-13 小结
依赖管理的核心就三处:顶层 dependencies 用 .package 按 URL 和版本区间声明;版本区间里 from 最常用、exact/..< 更精确、branch/revision 应急用;target 里用 .product 把需要的库接进来。记得提交 Package.resolved 锁定版本。下两章我们讲怎么给包加测试和做基础调试。
Tip加完依赖后第一次
swift build若卡很久,多半是在下载并编译依赖本身(尤其像 swift-argument-parser 这类还带 transitive 依赖的)。耐心等一次,后续有缓存就快了。