一、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。
三、插件能力总览(官方支持)
- 读取
context.package
下所有 Manifest 信息(target、product、dependency) - 读文件系统做统计(行数、文件数)
- 申请
.writeToPackageDirectory(reason:)
回写 README - 支持
async throws
,可调用任意 CLI(git、grep、wc、mermaid-cli ...)
四、手把手落地:GeneratePackageMetadata
- 创建 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)")
}
五、使用方式(客户端工程)
- 把插件包当依赖
Xcode中添加本地依赖

也可以在Package.swift中添加依赖
swift
// 客户端 Package.swift
dependencies: [
.package(path: "../SwiftPluginResources")
]
- 对任意 target 挂上插件(Command 插件不要求跟 target 有编译依赖,挂谁都可以)
swift
.target(
name: "MyPackage",
dependencies: [
.product(
name: "GeneratePackageMetadata",
package: "SwiftPluginResources")
]
),
- 运行
- CLI:
bash
swift package generate-package-metadata
-
Xcode 15+:
Package Dependencies
→ 右键插件包 →Generate Package Metadata
六、扩展场景清单
- 自动生成
CHANGELOG.md
(读取 git tag 与 commit message) - 扫描
public
API → 输出 SemVer 兼容的 API-Breaking 报告 - 结合 Mermaid Live Editor 生成在线可访问的依赖热力图
- 把统计结果上传 Notion 数据库,做全局包健康度看板
- 在 CI 中先运行插件,再检测 README 是否有未提交的 diff,强制开发者同步文档
七、其他开源command plugin