Meet Swift Package plugins
Swift & UI 进阶 20m

认识 Swift Package 插件

Meet Swift Package plugins

2022年6月6日

在 Apple 官方观看视频

一句话判断

Swift Package Plugins 让你用纯 Swift 脚本自动化开发工作流——代码格式化、文档生成、版权更新——全部集成在 Xcode 14 中,无需额外工具。

这场 Session 讲了什么

Xcode 14 引入了 Swift Package Plugins,这是一种用 Swift 编写的脚本,可以对你的 Swift Package 或 Xcode Project 执行操作。插件分为两类:Command Plugins(命令插件)和 Build Tool Plugins(构建工具插件)。

Command Plugins 用于开发工作流自动化,比如运行代码格式化工具(SwiftFormat)、根据 Git 历史生成贡献者列表、更新源文件中的版权日期。Build Tool Plugins 扩展构建系统的依赖图,用于在构建过程中生成源代码或资源文件。

插件运行在沙盒中,默认没有网络访问权限,文件写入也受限。Command Plugins 可以声明需要文件写入权限,Xcode 会向用户请求确认。插件通过 PackagePlugin 模块提供的 API 访问 Package 的源文件和结构信息。

Session 完整演示了如何使用现成的插件(基于 Xcode 的右键菜单),以及插件的工作原理和架构。

值得深挖的点

  • 插件的依赖管理:插件可以依赖其他 Package 中的工具(比如 SwiftFormat)。Xcode 会自动构建这些工具。工具可以是源码形式也可以是二进制形式。
  • 沙盒安全模型:插件默认无法访问网络,只能写入特定的输出目录。需要修改源文件的插件必须声明权限,用户批准后才能运行。Xcode 会记住用户的选择。
  • 插件与工具的分离:提供插件的 Package 和提供底层工具的 Package 可以是不同的。比如一个通用工具插件 Package 可以依赖 SwiftFormat,而你不需要修改 SwiftFormat 的源码。
  • Private vs Public 插件:插件可以只在定义它的 Package 内部使用(private),也可以作为 Product 对外发布(public),让其他 Package 通过依赖声明使用。

代码片段

// Package.swift 中定义插件
// .plugin 目标声明
.product(
    name: "MyPackage",
    targets: [
        // 定义一个命令插件
        .plugin(
            name: "GenerateContributors",
            capability: .command(
                intent: .sourceCodeEditing(),
                permissions: [
                    .writeToPackageDirectory(
                        reason: "需要创建 CONTRIBUTORS 文件"
                    )
                ]
            ),
            dependencies: [
                // 插件可以依赖外部工具
                .product(name: "SwiftFormat", package: "SwiftFormat")
            ]
        ),
        // 定义一个构建工具插件
        .plugin(
            name: "GenerateSwiftCode",
            capability: .buildTool()
        )
    ]
)
// 插件的主入口——Command Plugin 示例
import PackagePlugin

struct GenerateContributors: CommandPlugin {
    func performCommand(
        context: PluginContext,
        arguments: [String]
    ) async throws {
        // 获取 Package 的源文件
        let package = context.package

        // 调用 Git 获取贡献者信息
        let gitTool = try context.tool(named: "git")
        let process = Process()
        process.executableURL = URL(fileURLWithPath: gitTool.path.string)
        process.arguments = ["log", "--format='%an'", "|", "sort", "-u"]
        try process.run()
        process.waitUntilExit()

        // 将结果写入 CONTRIBUTORS 文件
        // (需要声明 writeToPackageDirectory 权限)
    }
}

最佳实践

  • 优先搜索社区已有的插件,不要重复造轮子
  • 需要修改源文件的插件要给出清晰的 reason 说明,让用户信任你的操作
  • 插件脚本要处理错误情况,给出有意义的错误信息
  • Build Tool Plugins 要注意增量构建——只在输入变化时重新生成
  • 把插件作为 Product 发布可以让整个社区受益

还有什么值得关注

  • “Create Swift Package plugins” 提供了完整的插件开发教程
  • SwiftFormat、SwiftLint 等流行工具已经提供了插件适配
  • Xcode 14 的插件支持也适用于 Xcode Project(不仅限于 Swift Package)
  • 插件机制为代码生成的自动化(比如 Protocol Buffer、GraphQL)提供了标准化的方案
WWDC 2022