Create Swift Package plugins
Swift & UI 进阶 20m

创建 Swift Package 插件

Create Swift Package plugins

2022年6月6日

在 Apple 官方观看视频

一句话判断

Swift Package Plugins 让你用 Swift 代码扩展 Xcode 和 SwiftPM 的构建流程——自定义命令、代码生成、自动化发布任务,这场 Session 通过三个 Demo 展示了从入门到进阶的完整路径。

这场 Session 讲了什么

Boris 主讲的这场 Session 系统性地介绍了 Swift Package Plugins 的创建方式。从基本的自定义命令插件(如从 Git 历史生成 CONTRIBUTORS 文件),到构建工具插件(分为 in-build 和 pre-build 两种类型),再到权限声明和沙盒机制。

核心概念:插件是用 Swift 编写的代码,使用 PackagePlugin API(类似 Package Manifest)。插件运行在沙盒中,网络访问受限,默认只能写临时目录。自定义命令可以选择声明写入 Package 根目录的权限。构建工具插件通过声明输入/输出关系,让构建系统自动判断是否需要重新执行。

值得深挖的点

In-build vs Pre-build 的选择。 In-build command 有明确的输出文件集合,构建系统会比较输入和输出的时间戳,只在输出过期时重新执行。Pre-build command 每次构建都会运行。如果你的工具有明确的输出(如代码生成),用 in-build;如果没有(如 lint 检查),用 pre-build,但要注意缓存策略避免性能问题。

沙盒限制和权限声明。 插件运行在受限环境中,网络访问被禁止。自定义命令可以通过 permission 声明需要写入 Package 根目录的权限,Xcode 会展示 reason string 让用户决定是否授权。这个设计类似 iOS 的权限弹窗,安全性考虑一致。

构建工具插件的三段式结构。 插件定义 executable(要运行的工具)、输入文件(如 .proto 文件)、输出文件(如生成的 .swift 文件)。构建系统根据这些信息自动调度执行时机。

代码片段

// Package.swift 中声明插件目标
// swift-tools-version: 5.6
.package(
    name: "MyPackage",
    targets: [
        .plugin(
            name: "GenerateContributors",
            capability: .command(
                intent: .custom(
                    verb: "generate-contributors",
                    description: "从 Git 历史生成 CONTRIBUTORS.txt"
                ),
                permissions: [
                    .writeToPackageDirectory(
                        reason: "需要写入 CONTRIBUTORS.txt 文件到 Package 根目录"
                    )
                ]
            )
        )
    ]
)
// plugin.swift - 自定义命令插件实现
import PackagePlugin
import Foundation

@main
struct GenerateContributors: CommandPlugin {
    func performCommand(
        context: PluginContext,
        arguments: [String]
    ) async throws {
        // 执行 Git 命令获取提交历史
        let process = Process()
        process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
        process.arguments = ["log", "--format='%aN'", "--sort=name"]
        let pipe = Pipe()
        process.standardOutput = pipe
        try process.run()
        process.waitUntilExit()

        let data = pipe.fileHandleForReading.readDataToEndOfFile()
        let output = String(data: data, encoding: .utf8) ?? ""
        // 去重并写入文件
        let contributors = Set(output.components(separatedBy: "\n"))
            .filter { !$0.isEmpty }
            .sorted()
        let content = contributors.joined(separator: "\n")
        try content.write(
            toFile: context.package.directoryURL
                .appendingPathComponent("CONTRIBUTORS.txt").path,
            atomically: true, encoding: .utf8
        )
    }
}

最佳实践

  • Tools version 最低 5.6。 插件功能从这个版本开始支持。
  • 谨慎使用 pre-build command。 每次构建都会执行,如果没有缓存机制,会严重拖慢构建速度。
  • 为权限声明写清晰的 reason。 用户看到这段文字来决定是否信任你的插件,模糊的描述会导致用户拒绝授权。
  • 利用 try process.run() 执行外部工具。 插件可以通过 Process API 调用 Git、SwiftFormat 等命令行工具,但要注意工具必须在沙盒限制下正常工作。

还有什么值得关注

  • 推荐先看 “Meet Swift Package Plugins” 了解基础概念
  • 构建工具插件可以包装 SwiftLint、SwiftFormat、Sourcery 等现有工具
  • 插件的输入输出声明帮助 Xcode 的增量构建系统正确判断是否需要重新执行
  • 插件源码放在 Package 的 Plugins/ 目录下,与 Sources/Tests/ 平级
WWDC 2022