如何使用 Swift Package Manager 开发命令行工具?
最近准备魔改一下 R.swift 以支持 Pod 库生成对应的
R.generated.swift
文件。经研究后发现,R.swift 的本质是使用
Swift Package Manager(简称 SPM) 开发了一个命令行工具
rswift
。很显然,要想魔改 R.swift,必须要学习如何使用 Swift
Package Manager
来开发命令行工具。本文,则通过一个简单的例子来对此进行介绍。
初始化
假设,我们要开发一个命令行工具:filecreator
,能够根据输入的文件名创建对应的文件。
首先,我们可以通过以下命令来创建并初始化项目。
1 | $ mkdir FileCreator |
其中,--type executable
表示我们要创建的是一个可执行文件(本文创建一个命令行工具),而不是一个
framework。
当初始化完毕之后,项目目录下会生成一系列文件:
Package.swift
:一个描述文件,定义了 Package 的基本信息以及依赖。Sources/
:源码目录。初始化时会自动生成一个main.swift
文件,即 命令行工具的入口。Tests/
:测试目录,存放测试代码。.gitignore
:默认忽略了 SPM 的编译目录.build
以及 Xcode 项目相关文件。
代码拆分
对于开发 SPM 可执行文件,一般建议将源代码拆分为两部分,分别用于创建 framework 和可执行文件。这样做不但可以让测试变得更加简单,而且可以让包含核心功能的 framework 作为其他工具的依赖库。
对此,我们可以在 Sources
目录下创建一个用于 framework
的目录,如下所示: 1
2$ cd Sources
$ mkdir FileCreatorCore
由于 SPM 使用文件系统作为编译参照,我们可以通过创建一个新的目录来定义一个新的模块。
接下来,更新 Package.swift
来定义两个 target:一个用于
FileCreator
,一个用于
FileCreatorCore
,如下所示: 1
2
3
4
5
6
7
8
9
10
11
12import PackageDescription
let package = Package(
name: "FileCreator",
targets: [
.target(
name: "FileCreator",
dependencies: ["FileCreatorCore"]
),
.target(name: "FileCreatorCore")
]
)
其中,描述文件中定义了 FileCreator
依赖
FileCreatorCore
。
基于 Xcode 开发
由于 Xcode 支持代码补全、调试、运行等功能,开发者会倾向于使用 Xcode
进行开发。对此,SPM 提供了一条命令可以让我们快速生成一个 xcode
项目。与之对应,.gitignore
文件也默认忽略了相关的 xcode
文件。
在项目的根目录下,我们可以通过如下命令生成 Xcode 项目。
1
$ swift package generate-xcodeproj
程序入口
我们知道在 c 语言开发中所有程序都有一个 main
入口函数。这里,虽然没有定义 main
函数,但是定义了一个
main.swift
文件(因此我们不能修改 main.swift
的文件名)。一般而言,我们建议将核心实现逻辑放在单独一个文件中,而核心调用逻辑则放在
main.swift
中。
对此,我们在 Sources/FileCreatorCore
目录下创建一个核心实现文件 FileCreatorCore.swift
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14// Sources/FileCreatorCore/FileCreatorCore.swift
import Foundation
public final class FileCreator {
private let arguments: [String]
public init(arguments: [String] = CommandLine.arguments) {
self.arguments = arguments
}
public func run() throws {
print("Hello world")
}
}
然后,在 main.swift
中调用 run()
方法。
1
2
3
4
5
6
7
8
9import FileCreatorCore
let tool = FileCreator()
do {
try tool.run()
} catch {
print("Whoops! An error occurred: \(error)")
}
编译运行
接下来,我们来对项目进行编译并运行,命令分别如下: 1
2
3$ swift build
$ swift run
> Hello world
添加依赖
和 iOS 开发一样,我们开发 swift package 时也会依赖第三方的 swift
package 或 framework。依赖关系则定义在 Package.swift
中,如下所示: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import PackageDescription
let package = Package(
name: "FileCreator",
dependencies: [
.package(
name: "Files",
url: "https://github.com/johnsundell/files.git",
from: "4.0.0"
)
],
targets: [
.target(
name: "FileCreator",
dependencies: ["FileCreatorCore"]
),
.target(
name: "FileCreatorCore",
dependencies: ["Files"]
)
]
)
其中,Files
是一个基于 swift
编写的第三方框架,支持简单的处理文件和目录。我们在
fileCreator
中使用 Files
实现在当前目录下创建文件。
安装依赖
当声明的依赖发生变化后,我们可以使用 SPM 来解析并安装依赖,然后重新生成 Xcode 项目。
1 | $ swift package update |
安装完依赖后,会生成一个 Package.resolved
文件,这是 SPM
生成的锁存文件,记录了依赖更新后的全版本列表。
读取参数
接下来,我们来修改
Sources/FileCreatorCore/FileCreator.swift
,替换掉
print "Hello world"
,实现通过传入参数创建文件的功能。
1 | import Foundation |
测试
由于之前我们将命令行工具的实现才分为了两部分:framework 和可执行文件,因此,测试也变得非常简单。我们要做的仅仅是运行并断言创建了相应的文件即可。
首先,我们在 Package.swift
中声明一个测试模块,如下所示: 1
2
3
4.testTarget(
name: "FileCreatorTests",
dependencies: ["FileCreatorCore", "Files"]
)
然后,重新生成 Xcode 项目。 1
$ swift package generate-xcodeproj
重新使用 Xcode 打开项目,修改 FileCreatorTests.swift
。
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
29import Foundation
import XCTest
import Files
import FileCreatorCore
class FileCreatorTests: XCTestCase {
func testCreatingFile() throws {
// Setup a temp test folder that can be used as a sandbox
let tempFolder = Folder.temporary
let testFolder = try tempFolder.createSubfolderIfNeeded(
withName: "FileCreatorTests"
)
// Empty the test folder to ensure a clean state
try testFolder.empty()
// Make the temp folder the current working folder
let fileManager = FileManager.default
fileManager.changeCurrentDirectoryPath(testFolder.path)
// Create an instance of the command line tool
let arguments = [testFolder.path, "Hello.swift"]
let tool = FileCreator(arguments: arguments)
// Run the tool and assert that the file was created
try tool.run()
XCTAssertNotNil(try? testFolder.file(named: "Hello.swift"))
}
}
最后,执行 swift test
即可进行测试。
安装命令行工具
截止现在,我们已经构建并测试了我们的命令行工具,接下来我们来安装它,使得能够在命令行进行全局调用。对此,我们需要使用
release 配置进行构建,然后将编译产出的二进制放到
/usr/local/bin
目录下。
1 | $ swift build -c release |