Skip to main content

Crate pmpx_plugin

Crate pmpx_plugin 

Source
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起子进程、发网络请求

三条理由:

  1. command() 因此是完全纯的(输入只有动词、参数、命中文件列表),单测不需要 任何 fixture 目录;
  2. 插件不能借文件读取去探测不该知道的东西;
  3. “项目长什么样“本来就该由检测层决定 —— 那是宿主的职责,也是最该集中在一处的 东西。

matched 是宿主在检测阶段顺手带下来的(它本来就要 stat 那些文件),所以插件拿到的 信息零额外成本。代价是新增一种形态判断要在 manifest 里多声明一个文件名 —— 换来的是“插件能看到什么“变成一份声明式、可审计的白名单。

§边界上为什么不能有 Rust 类型

宿主与插件是两个独立编译的世界。穿过 abi 的数据一律是 #[repr(C)] 的 POD, 所以两边不需要同一个 rustc,也不需要共享分配器。详见 abi 的模块文档。

Modules§

abi
跨 dlopen 边界的线格式。

Macros§

export
生成插件的整个 C ABI 外壳。

Structs§

CommandSpec
一条待执行的命令。
Context
宿主传给插件的上下文。只读。
Family
生态分组。

Enums§

PluginError
插件能报出来的错误。
Verb
pmpx 认可的动词。

Traits§

PackageManager
一个包管理器后端的实现。