Expand description
§pmpx-plugin
pmpx 的插件契约:一个 trait,加一条稳定的 C ABI,把 trait 安全地送过 dlopen 边界。
插件作者只需要实现 PackageManager,然后用一行 export!
生成整个 C ABI 外壳:
ⓘ
use pmpx_plugin::{CommandSpec, Context, Family, PackageManager, PluginError, Verb};
use std::ffi::OsString;
struct CargoPlugin;
impl PackageManager for CargoPlugin {
fn name(&self) -> &str { "cargo" }
fn family(&self) -> Family { Family::RUST }
fn command(
&self,
_ctx: &Context,
verb: Verb,
args: &[OsString],
) -> Result<CommandSpec, PluginError> {
let spec = match verb {
Verb::Install if args.is_empty() => CommandSpec::new("cargo").arg("fetch"),
Verb::Install => CommandSpec::new("cargo").arg("add").args(args.iter()),
Verb::Remove => CommandSpec::new("cargo").arg("remove").args(args.iter()),
// cargo 没有 exec 语义 —— 明确声明不支持,宿主会退化成裸透传
Verb::Exec => return Err(PluginError::unsupported_verb(verb)),
other => return Err(PluginError::other(format!("还没实现 {other}"))),
};
Ok(spec)
}
}
pub fn create() -> Box<dyn PackageManager> { Box::new(CargoPlugin) }
pmpx_plugin::export!(create);§插件能做什么,不能做什么
PackageManager::command 只应该依据入参做映射。具体地:
| 允许 | 禁止 |
|---|---|
依据 Verb / args 分支 | 读任何文件(包括 project_root 下的) |
依据 Context::matched 分支 | 写任何文件 |
| 纯内存计算、字符串拼装 | 读环境变量 |
构造 CommandSpec | 起子进程、发网络请求 |
三条理由:
command()因此是完全纯的(输入只有动词、参数、命中文件列表),单测不需要 任何 fixture 目录;- 插件不能借文件读取去探测不该知道的东西;
- “项目长什么样“本来就该由检测层决定 —— 那是宿主的职责,也是最该集中在一处的 东西。
matched 是宿主在检测阶段顺手带下来的(它本来就要 stat 那些文件),所以插件拿到的
信息零额外成本。代价是新增一种形态判断要在 manifest 里多声明一个文件名 ——
换来的是“插件能看到什么“变成一份声明式、可审计的白名单。
§边界上为什么不能有 Rust 类型
宿主与插件是两个独立编译的世界。穿过 abi 的数据一律是 #[repr(C)] 的 POD,
所以两边不需要同一个 rustc,也不需要共享分配器。详见 abi 的模块文档。
Modules§
- abi
- 跨
dlopen边界的线格式。
Macros§
- export
- 生成插件的整个 C ABI 外壳。
Structs§
- Command
Spec - 一条待执行的命令。
- Context
- 宿主传给插件的上下文。只读。
- Family
- 生态分组。
Enums§
- Plugin
Error - 插件能报出来的错误。
- Verb
- pmpx 认可的动词。
Traits§
- Package
Manager - 一个包管理器后端的实现。