- 开发工具
- 构建工具
【免费下载链接】swift-package-manager
The Package Manager for the Swift Programming Language
命令插件(Command Plugin)是 Swift Package Manager 提供的两大插件扩展点之一,它允许你为swift package命令注入自定义子命令,在任意时刻执行与构建流程无关的脚本化操作(如源码格式化、文档生成、代码分析)。本文以官方文档 WritingCommandPlugin.md 为核心骨架,结合本仓库中PackagePlugin运行时的真实源码(Plugin.swift、Protocols.swift)与仓库内测试 Fixture,系统讲解命令插件的声明方式、工具依赖、实现要点、诊断 API、调试策略及 Xcode 扩展机制,帮助你从零写出可分发、可复用、可被 IDE 识别的生产级命令插件。
命令插件:为swift package扩展自定义命令
编写包插件的第一步,是判断你需要的插件类型。Swift Package Manager 定义了两种插件扩展点(详见 Plugins.md):
- 构建工具插件(Build Tool Plugin):在每次构建开始或构建过程中运行自定义任务,通常用于生成源文件。适用于"构建期间必须执行"的场景。
- 命令插件(Command Plugin):由用户随时通过
swift package <command> <arguments>主动调用,与构建图(build graph)无关,通常通过启动命令行工具子进程来完成实际工作。
两者在声明方式上非常相似,区别在于:命令插件声明.command()capability,并在插件脚本中实现与构建工具插件不同的入口函数。如果你需要的是在每次构建时生成源文件,应实现构建工具插件,参见 WritingBuildToolPlugin.md。
命令插件通过intent(语义意图)声明命令的用途——可以是"文档生成"、"源码格式化"等预定义意图,也可以是携带自定义动词(verb)的自定义意图,该动词会作为swift package的子命令被调用。此外,命令插件还可以声明所需的特殊权限,例如修改包目录下文件的权限。
意图声明(intent)提供了一种按功能类别对命令插件分组的机制,使包管理器或支持 SwiftPM 包的 IDE 能够按用途展示可用命令。例如,多个不同的插件都可以生成包文档,但通过意图声明,这些命令可以被统一分组、发现。
插件的可用范围遵循以下规则:插件对定义它的包本身可用;如果还声明了对应的插件产品(plugin product),则对任何直接依赖该包的其他包同样可用。
在 Package.swift 中声明命令插件
一个声明了命令插件的包的 manifest 如下(完整示例来自官方文档):
import PackageDescription let package = Package( name: "MyPluginPackage", products: [ .plugin( name: "MyCommandPlugin", targets: [ "MyCommandPlugin" ] ) ], dependencies: [ .package( url: "https://github.com/example/sometool", from: "0.1.0" ) ], targets: [ .plugin( name: "MyCommandPlugin", capability: .command( intent: .sourceCodeFormatting(), permissions: [ .writeToPackageDirectory(reason: "This command reformats source files") ] ), dependencies: [ .product(name: "SomeTool", package: "sometool"), ] ) ] )上述示例中,插件声明其用途为源码格式化(.sourceCodeFormatting()),并声明需要修改包目录中文件的权限。要点拆解如下:
| 声明项 | 作用 |
|---|---|
.plugin(name:capability:dependencies:)目标 | 定义一个插件目标;capability决定插件属于哪个扩展点及入口 |
products: [.plugin(name:targets:)] | 插件产品,使插件对其他直接依赖本包的包可见 |
intent: .sourceCodeFormatting() | 预定义意图之一,也可用.custom(verb:description:)自定义命令动词 |
permissions: [.writeToPackageDirectory(reason:)] | 声明写包目录权限;reason是展示给用户的批准理由 |
dependencies: [.product(...)] | 插件可用的可执行工具依赖 |
仓库中的 Fixture 提供了大量真实声明示例。例如 CommandPluginTestStub/Package.swift 展示了swift-tools-version: 5.9下使用自定义意图声明多个命令插件的写法:
// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "CommandPluginDiagnostics", targets: [ .plugin( name: "diagnostics-stub", capability: .command(intent: .custom( verb: "print-diagnostics", description: "Writes diagnostic messages for testing" )) ), .plugin( name: "plugin-dependencies-stub", capability: .command(intent: .custom( verb: "build-plugin-dependency", description: "Build a plugin dependency for testing" )), dependencies: [ .target(name: "plugintool") ] ), // ...其他插件目标 .executableTarget(name: "plugintool") ] )而 CommandPluginCompilationError/Package.swift 演示了swift-tools-version: 5.6下的最小声明,其中命令动词为my-build-tester:
.plugin( name: "MyCommandPlugin", capability: .command( intent: .custom(verb: "my-build-tester", description: "Help description") ) )沙箱与权限模型
包管理器在沙箱中运行插件,默认阻止网络访问和绝大多数文件系统写入。插件声明额外权限后,包管理器会在获得用户批准后,才授予网络访问或指定的文件系统访问权限(详见 Plugins.md)。具体到命令插件:
- 所有插件都可以写入一个临时目录(即
PluginContext中的pluginWorkDirectory,见下文); - 需要修改包源码的命令插件可声明写包目录权限,用户批准后才会授予对包目录的写访问;
- 构建工具插件则不能修改包源码。
在Plugin.swift的头部注释中,可以印证沙箱机制的具体实现:插件宿主(SwiftPM 或使用 libSwiftPM 的 IDE)把组成插件的 Swift 源文件编译成面向宿主平台的可执行文件,然后在阻止网络访问、只允许少数特定文件系统位置的沙箱中调用该可执行文件(见 Plugin.swift)。
声明插件工具依赖:tool(named:)的查找逻辑
当插件需要同时工作在 SwiftPM、IDE 及其他宿主环境中时,应把命令行工具声明为插件目标的直接依赖。随后,根据所声明依赖的类型,将以下名称之一传给PluginContext.tool(named:):
- 可执行目标(executable target)的目标名;
- 可执行产品(executable product)的产品名;
- 可执行 artifact(binary artifact bundle 中)的 artifact 名。
需要注意两点平台约束:可执行依赖是为宿主平台构建的;如果使用二进制 artifact bundle,其必须提供支持宿主平台的变体(variant)。
tool(named:)的完整查找逻辑可以在 Context.swift 中看到:插件宿主首先在插件目标的直接依赖中查找匹配的工具;若没有找到,则继续在宿主提供的附加搜索目录中查找。tool(named:)的文档注释还明确指出:
- 工具名区分大小写;
- 若声明的二进制工具没有宿主平台变体,抛出
PluginContextError.toolNotSupportedOnTargetPlatform(name:); - 若没有匹配工具,抛出
PluginContextError.toolNotFound(name:)。
关于 CLI 的回退搜索:当包管理器从命令行调用命令插件、且没有声明的依赖匹配时,它会依次搜索"选定的 Swift 编译器所在目录"和"调用进程的PATH目录"。此回退行为是 SwiftPM 命令行界面特有的——IDE 和其他宿主可能提供不同的搜索目录。因此,如果插件故意要求使用用户自行安装的工具,请务必在文档中说明该要求,并在PluginContext.tool(named:)找不到工具时输出清晰的诊断信息。
实现命令插件脚本
实现命令插件的源码应放在包的Plugins子目录下,并将插件入口结构体(struct)声明为遵循CommandPlugin协议。CommandPlugin协议定义于 Protocols.swift:
public protocol CommandPlugin: Plugin { func performCommand( context: PluginContext, arguments: [String] ) async throws /// 一个指向 SwiftPM 或托管命令插件的 IDE 的代理, /// 插件可通过它请求专门的信息或动作。 var packageManager: PackageManager { get } }官方文档给出的完整命令插件实现示例(实现一个调用sometool的源码格式化命令)如下:
import PackagePlugin import Foundation @main struct MyCommandPlugin: CommandPlugin { func performCommand( context: PluginContext, arguments: [String] ) throws { // 要调用 `sometool` 格式化代码,先定位它。 let sometool = try context.tool(named: "sometool") // 按惯例使用包根目录下的配置文件,让包所有者可以把 // 格式设置提交到仓库。 let configFile = context .package .directory .appending(".sometoolconfig") // 提取目标参数(如果没有,则假定为全部目标)。 var argExtractor = ArgumentExtractor(arguments) let targetNames = argExtractor.extractOption(named: "target") let targets = targetNames.isEmpty ? context.package.targets : try context.package.targets(named: targetNames) // 遍历提供的目标并逐一格式化。 for target in targets { // 跳过任何没有源文件的目标类型。 // 注意:这里也可以改为发出警告或错误。 guard let target = target.sourceModule else { continue } // 对目标目录调用 `sometool`,并传入包目录中的配置文件。 let sometoolExec = URL(fileURLWithPath: sometool.path.string) let sometoolArgs = [ "--config", "\(configFile)", "--cache", "\(context.pluginWorkDirectory.appending("cache-dir"))", "\(target.directory)" ] let process = try Process.run(sometoolExec, arguments: sometoolArgs) process.waitUntilExit() // 检查子进程调用是否成功。 if process.terminationReason == .exit && process.terminationStatus == 0 { print("Formatted the source code in \(target.directory).") } else { let problem = "\(process.terminationReason):\(process.terminationStatus)" Diagnostics.error("Formatting invocation failed: \(problem)") } } } }关键设计点逐段解读
1. 入口与上下文(context)。与一次只作用于单个包目标的构建工具插件不同,命令插件不一定只操作单个目标。context参数提供了对输入的访问,包括以命令插件所作用的包为根的一棵"蒸馏后"的包图(package graph)。PluginContext结构定义于 Context.swift,核心成员包括:
package:插件所作用的包的信息;pluginWorkDirectory/pluginWorkDirectoryURL:一个可写目录的路径/URL,插件或它构造的构建命令可以把任何内容写在这里(如生成文件、缓存),包管理器会在构建之间保留该目录内容,插件对目录中的内容有完全控制权;tool(named:):查找插件可用的命令行可执行文件(见上文)。
2. 参数处理与--target约定。命令插件可以接收参数,用来控制插件行为或进一步缩小操作范围。示例遵循了传递--target来把插件作用域限制到包中一组目标的惯例:若未传--target,则作用于context.package.targets(全部目标);若传了,则通过context.package.targets(named:)解析。插件只能使用标准系统库,不能使用其他包提供的库(如SwiftArgumentParser),因此示例使用了PackagePlugin模块内置的ArgumentExtractor辅助类型来提取参数。
ArgumentExtractor的实现位于 ArgumentExtractor.swift,是一个"简易"的参数提取工具,值得注意的行为有:
- 支持
--<name> <value>与--<name>=<value>两种选项形式(extractOption(named:)); - 支持
--<name>标志计数(extractFlag(named:),返回出现次数); - 只处理长选项形式,不支持
-n这类短形式; - 把第一个
--之后的所有参数视为字面量(位置参数); - 未提取的剩余参数可通过剩余属性获取(源码注释中说明:不处理位置参数与选项同名的情况)。
3. 调用子进程与退出状态检查。插件通过Process.run启动外部工具,并用process.waitUntilExit()等待结束,随后检查terminationReason == .exit且terminationStatus == 0判断成功与否;失败时通过Diagnostics.error输出诊断。示例中把工具路径转换为URL(fileURLWithPath:)再传给Process.run,这与Plugin.swift中对不同平台的适配(如 Windows 下可执行文件带.exe后缀的查找逻辑,见 Context.swift)相配合,保证跨平台可用。
诊断 API:让失败可见、可定位
命令插件的入口函数被标记为throws,从入口抛出的任何错误都会使本次插件调用被标记为失败,该错误会展示给用户,因此错误信息应清晰描述问题所在。
此外,插件还可以使用PackagePlugin中的DiagnosticsAPI 发出警告(warning)和错误(error),并可选地携带指向某个文件的路径和行号。Diagnostics结构定义于 Diagnostics.swift,公开接口包括:
| API | 说明 |
|---|---|
Diagnostics.error(_:file:line:) | 输出错误诊断 |
Diagnostics.warning(_:file:line:) | 输出警告诊断 |
Diagnostics.remark(_:file:line:) | 输出备注/提示诊断 |
Diagnostics.emit(_:_:file:line:) | 以指定Severity(.error/.warning/.remark)输出 |
Severity枚举 | 诊断严重级别 |
file与line参数默认取#file和#line(即调用点的文件名与行号),使诊断信息能精确定位到插件源码中的具体位置。源码注释同时指出:"在发出一个或多个错误后,插件应返回非零退出码"。这提示了一个良好实践:抛出错误或用Diagnostics.error报告失败后,应让插件进程以非零状态退出,宿主据此判断调用失败。
在Plugin.swift的消息循环实现中可以看到宿主侧的处理:入口抛出的错误会被捕获,并转为Diagnostics.error输出,然后以exit(1)结束进程(见 Plugin.swift)。
调试与测试建议
Swift Package Manager 目前没有针对插件的专门调试与测试支持。官方文档给出的实用建议是:
- 许多插件本质上是"适配器",主要工作是构造命令行并调用真正干活的工具;
- 当插件中存在非平凡的代码时,好的做法是把这些代码抽到独立的源文件中,然后用带相对路径的符号链接把这些文件包含到单元测试目标里,从而复用并测试这些逻辑。
也就是说,插件的核心业务逻辑应尽量与PluginContext解耦,抽成纯函数/纯类型,便于在普通测试目标中直接验证,而不必真正驱动包管理器运行插件。
Xcode 扩展:XcodeProjectPlugin与条件编译
当在 Apple 的 Xcode IDE 中调用插件时,插件可以访问 Xcode 提供的一个库模块——XcodeProjectPlugin。该模块扩展了PackagePlugin的 API,让插件除了处理包之外,还能处理 Xcode 目标(Xcode project target)。
为了使插件在任意环境下都能处理 Swift 包,且在 Xcode 中运行时能条件性地处理 Xcode 工程,插件应在可用时条件性地导入XcodeProjectPlugin模块。官方文档示例:
import PackagePlugin @main struct MyCommandPlugin: CommandPlugin { /// 处理 Swift 包时调用此入口。 func performCommand(context: PluginContext, arguments: [String]) throws { debugPrint(context) } } #if canImport(XcodeProjectPlugin) import XcodeProjectPlugin extension MyCommandPlugin: XcodeCommandPlugin { /// 处理 Xcode 工程时调用此入口。 func performCommand(context: XcodePluginContext, arguments: [String]) throws { debugPrint(context) } } #endif要点说明:
XcodePluginContext输入结构与PluginContext类似,区别在于它提供了对 Xcode 工程的访问;Xcode 工程模型使用 Xcode 的命名与语义,与包管理器的模型略有不同;- 底层类型(如
FileList、Path)在PackagePlugin与XcodeProjectPlugin中是相同的,可以无缝共享; - 如果用户在 Xcode 界面中选中了目标,Xcode 会把目标名以
--target参数传给插件——这与命令行调用时--target的约定保持一致; - 其他使用包管理器的 IDE 或自定义宿主环境,同样可以提供定义新入口并扩展核心
PackagePluginAPI 的模块。
运行命令插件:发现、调用与权限放行
命令插件由用户通过swift package主动调用(相关指南见 EnableCommandPlugin.md,完整的swift package plugin子命令文档见 PackagePlugin.md)。
发现可用插件:
swift package plugin --list调用插件——在swift package后面跟上插件自定义动词,并追加所需参数。例如调用 swift-docc-plugin 的generate-documentation命令:
swift package generate-documentation传递参数与标志:包管理器会把调用动词之后的所有命令行参数与标志原样传给插件。例如只针对单个目标生成文档:
swift package generate-documentation --target MyTarget豁免沙箱约束:需要写文件系统的命令插件,在从控制台调用swift package时会请求用户批准;若非交互式环境则直接拒绝。可以通过标志免询问放行:
--allow-writing-to-package-directory:允许写入包目录(无需询问),在持续集成(CI)环境中尤其有用;--allow-network-connections:允许网络连接,不弹提示。
源码级原理:插件是如何被宿主驱动的
理解命令插件的底层运行机制,有助于写出行为可预期的插件。Plugin.main()是PackagePlugin运行时为所有类型插件提供的统一入口(见 Plugin.swift),其关键实现如下:
- 进程模型:每个插件都以独立进程运行,与包管理器分离(见 Plugins.md);
- 消息通道:宿主进程与插件通过"长度前缀的 JSON 编码 Swift 枚举"消息通信——宿主经插件标准输入管道发送消息,插件经标准输出管道回传消息;标准错误管道的输出被视为自由格式的控制台文本;
- 标准流重定向:插件进程内,
stdout被重定向到stderr(使插件里print的内容作为普通文本输出),stdin被关闭(插件逻辑若尝试从控制台读取会得到错误而非阻塞);原始的stdin/stdout文件描述符被复制出来专门用作消息管道; - 退出码语义:插件进程的退出码表示调用是否成功;失败结果应伴随一个错误诊断输出,以便用户理解出错原因;
- 沙箱与缓冲:支持沙箱的平台会限制网络与文件系统写入;Windows 上禁用缓冲,其他平台启用行缓冲以保证文本及时输出。
这套"以标准输入输出流做消息通道"的设计,避免了在沙箱中为其他通信渠道开特权,也是跨平台可移植性的关键。
小结
命令插件是 Swift Package Manager 中一类"按需执行、与构建无关"的扩展机制:通过.command()capability 与intent/permissions在 manifest 中声明,在Plugins目录下实现遵循CommandPlugin协议的入口,利用PluginContext提供的包图、工作目录与工具查找能力完成实际工作。写插件时需记住几个要点:
- 意图与权限要声明清楚——意图决定命令如何被分组发现,权限决定沙箱内可执行的操作范围;
- 工具依赖优先声明——直接依赖保证跨宿主可移植;依赖用户 PATH 的搜索属于 CLI 特有的回退路径,应明确文档化并输出清晰诊断;
- 入口抛错 +
Diagnostics双通道报告失败——错误信息要可理解、可定位; - 复杂逻辑抽离测试——插件本体保持"薄适配器",核心逻辑用符号链接纳入单元测试;
- Xcode 支持用条件导入——
#if canImport(XcodeProjectPlugin)下实现XcodeCommandPlugin以同时覆盖包与 Xcode 工程场景。
如需进一步了解构建工具插件、插件启用与运行细节,可继续阅读仓库内的 WritingBuildToolPlugin.md、EnableCommandPlugin.md 与 PackagePlugin.md;也可以在 Fixtures/Miscellaneous/Plugins 下查看大量真实可运行的命令插件声明与实现示例。
- 开发工具
- 构建工具
【免费下载链接】swift-package-manager
The Package Manager for the Swift Programming Language
相关推荐
Swift Package Manager 依赖编辑实战:swift package edit 命令完全指南
Swift Package Manager 依赖编辑实战:swift package edit 命令完全指南 swift package edit 是 Swif
开发工具构建工具Swift Package Manager 的 `swift package plugin` 命令:命令插件调用、权限控制与安全沙箱完全指南
Swift Package Manager 的 swift package plugin 命令:命令插件调用、权限控制与安全沙箱完全指南 swift packa
开发工具构建工具深入 Swift Package Manager 构建工具插件(Build Tool Plugin)开发指南
深入 Swift Package Manager 构建工具插件(Build Tool Plugin)开发指南 构建工具插件(build tool plugin)
开发工具构建工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考