pmpx_plugin/lib.rs
1//! # pmpx-plugin
2//!
3//! The pmpx plugin contract: one trait plus a stable C ABI that carries the trait safely across
4//! the `dlopen` boundary.
5//!
6//! A plugin author only implements [`PackageManager`] and then uses the one-line
7//! [`export!`](macro@crate::export) to generate the whole C ABI shell:
8//!
9//! ```ignore
10//! pub fn create() -> Box<dyn PackageManager> { Box::new(CargoPlugin) }
11//! pmpx_plugin::export!(create);
12//! ```
13//! # What a plugin may and may not do
14//!
15//! [`PackageManager::command`] should only map from its inputs: it does not read files (including
16//! anything under `project_root`), does not write files, does not read environment variables,
17//! does not spawn child processes, and does not make network requests. That keeps `command()`
18//! completely pure (unit tests need no fixture directory at all) and stops a plugin from using
19//! file reads to probe things it should not know.
20//!
21//! "What the project looks like" reaches a plugin through its **manifest**, in two declarative,
22//! auditable forms: the file *names* in `[detect]`, handed over as [`Context::matched`], and the
23//! file *contents* declared in `[context] files`, asked for one at a time through [`Context::file`]
24//! (or [`Context::file_str`]). A plugin that needs to know what a lockfile pins, or whether
25//! `package.json` mentions `packageManager`, declares that file and parses it itself; it never
26//! reaches for the filesystem, and pmpx never learns what is inside.
27//!
28//! Declaring a file is the *allowlist*, not a delivery: a name the manifest did not declare is
29//! never readable, and a file nobody asks for is never read.
30//! # Saying something
31//!
32//! A plugin that wants to explain itself calls [`debug!`](macro@crate::debug) /
33//! [`info!`](macro@crate::info) / [`warn!`](macro@crate::warn) / [`error!`](macro@crate::error),
34//! and the host decides what to print, adding the plugin's id. That is not only tidier than
35//! printing directly: since the *host* holds the switch, `--debug` never becomes an input a plugin
36//! could branch on, so a debug run executes the same command as any other. See
37//! [`debug`](mod@crate::debug) for the details, including what happens with no host installed (a
38//! plugin's own `cargo test`).
39//!
40//! Data crossing the boundary is always `#[repr(C)]` POD, so the two sides need not share a rustc;
41//! see the module docs of [`abi`]. A host asks the plugin for the **capabilities** it supports --
42//! `identity`, `command`, and optionally `attach` -- and passes a context whose every value is read
43//! through an accessor, which is what lets either side gain a key or a capability without a new
44//! contract version. [`shell`] is the plugin's end of that; `pmpx-loader` is the host's.
45//!
46//! The contract's parts live in sibling modules and are re-exported here, so every path that
47//! starts with `pmpx_plugin::` is stable: [`PackageManager`] and the [`Context`] it is called
48//! with, the [`CommandSpec`] it answers with, the [`Verb`]s, the [`Family`] and the
49//! [`PluginError`] it may report.
50#![deny(missing_docs)]
51#![warn(clippy::all)]
52
53/// The wire format, for a plugin that needs to talk to it directly (a hand-written shell, a test).
54pub use pmpx_plugin_abi as abi;
55
56pub mod debug;
57pub mod shell;
58
59mod context;
60mod error;
61mod export;
62mod family;
63mod manager;
64mod spec;
65mod verb;
66
67pub use context::{Context, ContextBuilder, ContextFile, SelectionReason};
68pub use error::PluginError;
69pub use family::Family;
70pub use manager::PackageManager;
71pub use spec::CommandSpec;
72pub use verb::Verb;