Swift Package Manager(包管理)
Swift Package Manager(简称 SwiftPM 或 SPM)是 Swift 官方的依赖管理与构建工具。
它负责三件事:描述一个包由哪些代码组成、从远端拉取依赖、把代码编译成库或可执行文件。
它已经集成进 Swift 工具链和 Xcode,不需要额外安装,是当前管理 Swift 项目依赖的首选方式。
SwiftPM 的核心概念
在动手之前,先把几个术语对应到实际的目录和文件上。
| 概念 | 对应实体 | 说明 |
|---|---|---|
| 包(package) | 一个含 Package.swift 的目录 | 分发和依赖管理的基本单位 |
| 清单(manifest) | Package.swift | 用 Swift 代码描述包的结构 |
| 目标(target) | Sources 下的一个子目录 | 一个编译模块,是代码的组织单位 |
| 产品(product) | products 数组中的条目 | 包对外提供的东西,库或可执行文件 |
| 依赖(dependency) | dependencies 数组中的条目 | 本包使用的其他包 |
最容易混淆的是「目标」和「产品」:目标是内部怎么组织代码,产品是外部能用什么。
一个目标可以不出现在任何产品里(纯内部实现),一个产品也可以由多个目标组成。
创建一个包
SwiftPM 自带脚手架命令,可以直接生成标准目录结构。
$ swift package init --type executable --name RunoobCLI Creating executable package: RunoobCLI Creating Package.swift Creating .gitignore Creating Sources Creating Sources/RunoobCLI/RunoobCLI.swift Creating Tests/ Creating Tests/RunoobCLITests/ Creating Tests/RunoobCLITests/RunoobCLITests.swift
生成的结构非常固定,源文件放在 Sources 下,测试放在 Tests 下。
RunoobCLI/
├── Package.swift
├── Sources/
│ └── RunoobCLI/
│ └── RunoobCLI.swift
└── Tests/
└── RunoobCLITests/
└── RunoobCLITests.swift
--type 支持多种模板,最常用的是 library、executable 和 empty,分别对应库、可执行程序和空包。
此外还有 tool(带命令行参数解析的可执行程序)、macro(宏包)、build-tool-plugin 等模板。
不指定名字时,SwiftPM 会使用当前目录名作为包名。
Package.swift 结构
清单文件本身就是一个 Swift 源文件,用 PackageDescription 提供的类型来描述包。
第一行的 swift-tools-version 是必须的,它决定清单文件能用哪些 API。
实例
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
// 包名,也是 Xcode 中显示的工程名
name: "RunoobKit",
// 声明支持的最低平台版本
platforms: [
.macOS(.v14)
],
products: [
// 库产品:供其他包或 App 依赖
.library(name: "RunoobCore", targets: ["RunoobCore"]),
// 可执行产品:swift run 运行的目标
.executable(name: "RunoobCLI", targets: ["RunoobCLI"]),
],
targets: [
// 库目标:只包含可复用的代码,没有 main 入口
.target(name: "RunoobCore"),
// 可执行目标:必须有 main 入口
.executableTarget(
name: "RunoobCLI",
dependencies: ["RunoobCore"]
),
// 测试目标:依赖被测模块
.testTarget(
name: "RunoobCoreTests",
dependencies: ["RunoobCore"]
),
],
// 使用 Swift 6 语言模式
swiftLanguageModes: [.v6]
)
常用参数的含义如下。
| 参数 | 是否必填 | 作用 |
|---|---|---|
| name | 必填 | 包名 |
| platforms | 可选 | 声明最低支持的系统版本 |
| products | 可选 | 对外提供的库或可执行文件 |
| dependencies | 可选 | 本包依赖的其他包 |
| targets | 必填 | 包内的所有目标 |
| swiftLanguageModes | 可选 | 指定使用的 Swift 语言模式 |
注意:不声明 platforms 时,SwiftPM 会按工具链支持的最低系统版本编译。如果代码里用到了较新的 API(例如 Synchronization 模块),一定要显式声明 platforms,否则会收到「API 在更低系统版本上不可用」的报错。
库目标与可执行目标
库目标用 .target 声明,里面放可复用的代码,不能有顶层可执行语句。
可执行目标用 .executableTarget 声明,必须提供 main 入口,可以用 @main 标注,也可以把文件命名为 main.swift。
两个目标之间的依赖通过 dependencies 参数声明,写的是目标名。
实例
import Foundation
/// 站点信息模型,供其他模块复用
public struct RunoobSite: Sendable {
public let name: String
public let url: String
public init(name: String, url: String) {
self.name = name
self.url = url
}
/// 返回「名称 (地址)」形式的描述
public func describe() -> String {
"\(name) (\(url))"
}
}
实例
import Foundation
import RunoobCore
@main
struct RunoobCLI {
static func main() {
let site = RunoobSite(name: "RUNOOB", url: "www.runoob.com")
print(site.describe())
}
}
可执行目标导入了库目标,就能直接使用 RunoobSite。
注意库目标里的类型和成员都写了 public,否则跨模块不可见,这是 SwiftPM 多目标项目最常见的报错来源。
依赖管理与版本约束
在 dependencies 数组里用 .package 声明依赖,版本约束的写法有多种。
| 写法 | 含义 | 适用场景 |
|---|---|---|
| .package(url: "…", from: "1.3.0") | 从 1.3.0 到下一个主版本之前 | 最常用,等价于 1.3.0..<2.0.0 |
| .package(url: "…", exact: "1.3.0") | 锁定到唯一版本 | 依赖有兼容性问题时临时使用 |
| .package(url: "…", "1.3.0"..<"1.6.0") | 指定区间 | 需要避开某个有问题的版本 |
| .package(url: "…", branch: "main") | 跟随分支 | 使用尚未发版的特性 |
| .package(url: "…", revision: "abc123") | 锁定到某个提交 | 临时验证某个 commit |
| .package(path: "../RunoobKit") | 本地路径依赖 | 同一台机器上的多包协同开发 |
下面是一个同时使用远端依赖和本地依赖的例子。
实例
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "RunoobApp",
platforms: [
.macOS(.v14)
],
dependencies: [
// 远端依赖:使用 1.3.0 及以上、2.0.0 以下的版本
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
// 本地依赖:同一台机器上的另一个包
.package(path: "../runoob-access-pkg"),
],
targets: [
.executableTarget(
name: "RunoobApp",
dependencies: [
// 引用远端依赖中的具体产品
.product(name: "ArgumentParser", package: "swift-argument-parser"),
// 引用本地依赖中的具体产品
.product(name: "RunoobKit", package: "runoob-access-pkg"),
]
),
]
)
依赖解析的结果会写进 Package.resolved 文件,它记录了每个依赖被解析到的确切版本。
这个文件应该提交到版本库,这样团队里所有人以及 CI 都能拿到完全一致的依赖版本。
常用命令
日常开发中真正高频的只有少数几个命令。
| 命令 | 作用 |
|---|---|
| swift build | 编译包,默认使用 debug 配置 |
| swift build -c release | 使用 release 配置编译 |
| swift run 目标名 | 编译并运行可执行目标 |
| swift test | 编译并运行测试目标 |
| swift package resolve | 解析依赖并更新 Package.resolved |
| swift package update | 把依赖更新到版本约束允许的最新版 |
| swift package describe | 打印包的名称、产品、目标结构 |
| swift package clean | 删除 .build 构建产物 |
下面是前面那个包的真实构建输出,其中省略了部分编译进度行,以及因机器而异的耗时数字。
$ swift build Building for debugging... [5/9] Emitting module RunoobCore [6/9] Compiling RunoobCore RunoobCore.swift [7/11] Compiling RunoobCLI RunoobCLI.swift [9/11] Linking RunoobCLI Build complete!
运行可执行目标时,如果代码有改动会先自动编译。
$ swift run RunoobCLI Build of product 'RunoobCLI' complete! RUNOOB (www.runoob.com)
swift package describe 适合用来确认依赖和目标的解析结果,尤其是依赖没被正确引用时。
$ swift package describe
Name: RunoobKit
Path: /Users/runoob/RunoobKit
Tools version: 6.0
Dependencies:
Platforms:
Name: macos
Version: 14.0
Products:
Name: RunoobCore
Type:
Library:
automatic
Targets:
RunoobCore
Name: RunoobCLI
Type:
Executable: nil
Targets:
RunoobCLI
Targets:
Name: RunoobCore
Type: library
Path: Sources/RunoobCore
测试目标用 swift test 运行,输出中会逐个列出用例的通过或失败情况。
实例
import Testing
@testable import RunoobCore
@Test func testDescribe() async throws {
let site = RunoobSite(name: "RUNOOB", url: "www.runoob.com")
#expect(site.describe() == "RUNOOB (www.runoob.com)")
}
这里用的是 Swift 官方新的 Testing 框架,Xcode 16 与新版工具链都自带,不再需要 XCTest。
与 Xcode 集成
Xcode 对 SwiftPM 是一等公民支持,几种用法都很直接。
双击 Package.swift,或者用 File > Open 打开包所在目录,Xcode 会把它当成一个工程,自动列出每个可执行目标对应的 scheme。
在已有的 App 工程里,用 File > Add Package Dependencies 可以直接按仓库地址添加依赖,Xcode 会帮你把版本约束写进工程配置。
添加后,Xcode 会把远端依赖缓存到本地,之后离线也能构建。
提示:在 Xcode 里编辑 Package.swift 时,如果改了目标结构但左侧列表没更新,用 File > Packages > Reset Package Caches 重置一次即可。
常见问题
库目标里的类型为什么在别的目标里用不了
检查是否漏写了 public。
每个目标都是独立模块,默认的 internal 级别只在模块内可见,跨目标必须提升到 public 或 open。
swift-tools-version 应该写多少
写你的工具链支持的最低版本即可,它决定了清单文件能用的 API。
写得太高,旧工具链无法打开这个包;写得太低,用不了新参数。
Package.resolved 要提交到版本库吗
对于可执行程序和 App,应该提交,用来保证所有人的依赖版本一致。
对于被广泛依赖的库,一般也建议提交,避免解析结果在不同环境漂移。
为什么 swift run 找不到我的目标
确认目标是用 .executableTarget 声明的,并且名字拼写一致。
只声明了 .target 的库目标不能被 swift run 运行。
本地路径依赖和远端依赖可以混用吗
可以,两者都写在 dependencies 数组里。
本地路径依赖常用于多包协同开发,发布前再换成带版本号的远端依赖。
