Swift Package Command Plugin 实战:一键生成 Package 元数据

一、Command Plugin 与 Build Tool Plugin 区别速览

维度 Command Plugin Build Tool Plugin
触发方式 手动 swift package plugin xxx或 Xcode UI 自动随 build 触发
沙盒权限 可申请写源码目录、联网 只读源码、不可联网
典型场景 文档、报表、格式化、post-processing 生成代码、资源、修改构建图

二、需求背景:模块化地狱

当 Xcode 工程拆成几十个 Swift Package 后,常遇到:

  • "这个包是谁维护?"
  • "它对外暴露哪些 Product?"
  • "依赖了哪些库?"
  • "代码量有多大?"

手动写 README 极易过期 → 用 Command Plugin 自动生成并回写 README.md

三、插件能力总览(官方支持)

  1. 读取 context.package 下所有 Manifest 信息(target、product、dependency)
  2. 读文件系统做统计(行数、文件数)
  3. 申请 .writeToPackageDirectory(reason:) 回写 README
  4. 支持 async throws,可调用任意 CLI(git、grep、wc、mermaid-cli ...)

四、手把手落地:GeneratePackageMetadata

  1. 创建 Package

可以通过Xcode的菜单进行创建

也可以通过命令行创建

bash 复制代码
mkdir PackageMetadataPlugin && cd $_
swift package init --type library --name SwiftPluginResources

Step 2 新增插件 Target 与产物

swift 复制代码
// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
let packageMetadataPluginName = "GeneratePackageMetadata"

let package = Package(
    name: "SwiftPluginResources",
    platforms: [.macOS(.v10_13)], // 这个插件仅在macOS上使用
    products: [
        .plugin(
            name: packageMetadataPluginName,
            targets: [packageMetadataPluginName]
        ),
    ],
    targets: [
        .plugin(
            name: packageMetadataPluginName,
            capability: .command(
                intent:.custom(
                    // 命令的名字
                    verb: "generate-package-metadata",
                    description: "Auto-generate README with metadata & Mermaid diagrams"
                ),
                permissions: [
                    // 写入package目录权限
                    .writeToPackageDirectory(
                        reason: "The plugin writes/updates the README.md file")
                ])
        ),
    ]
)

Step 3 入口文件(完整补注释版)

swift 复制代码
// Plugins/GeneratePackageMetadata/GeneratePackageMetadata.swift
import Foundation
import PackagePlugin

@main
struct GeneratePackageMetadata: CommandPlugin {
    func performCommand(context: PluginContext, arguments: [String]) async throws {
        var md = ""
        // 1. 基础信息
        md += try getPackageBaseInfo(context.package)
        // 2. 贡献者列表(git shortlog)
        md += try getContributors(at: context.package.directoryURL)
        // 3. 代码统计
        md += try getStatistics(for: context.package.directoryURL)
        // 4. Product 类图(Mermaid)
        md += try generateProductDiagram(context.package)
        // 5. 依赖关系图
        md += try generateDependencyDiagram(context.package)
        // 6. 写回 README
        try writeReadme(md, to: context.package.directoryURL)
    }
}

// MARK: - 1. 基础信息
private func getPackageBaseInfo(_ pkg: Package) throws -> String {
    // \($0.id) 的地方应该需要展示product的类型
    
    """
    # \(pkg.displayName)
    Generated on \(Date()).
    
    ## Products
    \(pkg.products.map { "- `\($0.name)` (\("$0.id"))" }.joined(separator: "\n"))
    
    ## Targets
    \(pkg.targets.map { "- `\($0.name)`" }.joined(separator: "\n"))
    
    """
}

// MARK: - 2. 贡献者
private func getContributors(at dir: URL) throws -> String {
    let process = Process()
    process.currentDirectoryURL = dir
    process.executableURL = URL(fileURLWithPath: "/usr/bin/git")
    process.arguments = ["shortlog", "-sn", "HEAD"]
    let out = Pipe()
    process.standardOutput = out
    try process.run()
    process.waitUntilExit()
    let data = out.fileHandleForReading.readDataToEndOfFile()
    let gitLog = String(data: data, encoding: .utf8) ?? "N/A"
    return """
    ## Contributors
    ```
    \(gitLog)
    ```
    
    """
}

// MARK: - 3. 代码统计
private func getStatistics(for dir: URL) throws -> String {
    let fm = FileManager.default
    let enumerator = fm.enumerator(atPath: dir.absoluteString)
    var files = 0, lines = 0
    while let file = enumerator?.nextObject() as? String {
        if file.hasSuffix(".swift") {
            files += 1
            let url = URL(fileURLWithPath: dir.absoluteString).appendingPathComponent(file)
            let content = try String(contentsOf: url, encoding: .utf8)
            lines += content.components(separatedBy: .newlines).count
        }
    }
    return """
    ## Statistics
    - Swift files: \(files)
    - Total lines: \(lines)
    
    """
}

// MARK: - 4. Product 类图(Mermaid)
private func generateProductDiagram(_ pkg: Package) throws -> String {
    // prod.type
    var diagram = "```mermaid\nclassDiagram\n"
    for prod in pkg.products {
        diagram += "    class \(prod.name) {\n        <<\("prodType")>>\n    }\n"
    }
    diagram += "```\n\n"
    return diagram
}

// MARK: - 5. 依赖图
private func generateDependencyDiagram(_ pkg: Package) throws -> String {
    var diagram = "```mermaid\ngraph TD\n"
    for dep in pkg.dependencies {
        diagram += "    \(pkg.displayName) --> \(dep.package.displayName)\n"
    }
    diagram += "```\n\n"
    return diagram
}

// MARK: - 6. 写回 README
private func writeReadme(_ content: String, to dir: URL) throws {
    let readmeURL = dir.appendingPathComponent("README.md")
    try content.write(to: readmeURL, atomically: true, encoding: .utf8)
    print("✅ README.md 已生成:\(readmeURL.path)")
}

五、使用方式(客户端工程)

  1. 把插件包当依赖

Xcode中添加本地依赖

也可以在Package.swift中添加依赖

swift 复制代码
// 客户端 Package.swift
dependencies: [
        .package(path: "../SwiftPluginResources")
    ]
  1. 对任意 target 挂上插件(Command 插件不要求跟 target 有编译依赖,挂谁都可以)
swift 复制代码
.target(
            name: "MyPackage",
            dependencies: [
                .product(
                    name: "GeneratePackageMetadata",
                    package: "SwiftPluginResources")
            ]
        ),
  1. 运行
  • CLI:
bash 复制代码
swift package generate-package-metadata
  • Xcode 15+:

    Package Dependencies → 右键插件包 → Generate Package Metadata

六、扩展场景清单

  1. 自动生成 CHANGELOG.md(读取 git tag 与 commit message)
  2. 扫描 public API → 输出 SemVer 兼容的 API-Breaking 报告
  3. 结合 Mermaid Live Editor 生成在线可访问的依赖热力图
  4. 把统计结果上传 Notion 数据库,做全局包健康度看板
  5. 在 CI 中先运行插件,再检测 README 是否有未提交的 diff,强制开发者同步文档

七、其他开源command plugin

  1. github.com/MarcoEiding...
  2. github.com/FelixHerrma...
  3. github.com/swiftlang/s...
相关推荐
低调小一10 小时前
Swift 语法学习指南 - 与 Kotlin 对比
微信·kotlin·swift
HarderCoder10 小时前
Swift Package Plugin 深度实战:从原理到落地,自动生成字体枚举
swift
东坡肘子18 小时前
从开放平台到受控生态:谷歌宣布 Android 开发者验证政策 | 肘子的 Swift 周报 #0101
android·swiftui·swift
HarderCoder18 小时前
用 `defer` 管理异步清理:Swift 中的“保险丝”模式
swift
大熊猫侯佩1 天前
冰火岛 Tech 传:Apple Foundation Models 心法解密(上集)
llm·ai编程·swift
HarderCoder1 天前
深入理解 SwiftUI 的 Structural Identity:为什么“换个条件分支”就会丢状态?
swiftui·swift
HarderCoder1 天前
Swift Continuations 完全指南:一口气弄懂 4 种“桥梁”
swift
HarderCoder1 天前
Swift 的 `withoutActuallyEscaping`:借一个 `@escaping` 身份,但不真的逃跑
swift
Swift社区1 天前
Swift 解法详解:LeetCode 371《两整数之和》
开发语言·leetcode·swift