Skip to main content

pmpx_engine/
backend.rs

1#![allow(unsafe_code)] // the one load call: this module is where the host crosses into a plugin
2//! Loading one plugin and asking it for a command: the host's half of the ABI.
3//!
4//! Everything below this line is the ABI's problem, and `pmpx-loader` is where the wire format lives.
5//! What this module adds is the *host's* decisions: which library to open, whether the plugin is the one
6//! the manifest promised, when to hand over the logging hooks, and what to do with what the plugin says
7//! while it runs. All of it is reported as [`Event`]s -- nothing here prints.
8
9use std::path::{Path, PathBuf};
10
11use pmpx_loader::{CallError, ContextSource, Plugin};
12use pmpx_plugin::abi::PmpxHost;
13use pmpx_plugin::{CommandSpec, SelectionReason, Verb};
14
15use crate::error::Result;
16use crate::files::Declared;
17use crate::log::{self, Levels};
18use crate::{EngineError, Event};
19
20/// One installed plugin, as this crate sees it: what the store read from its manifest, and where it
21/// lives. No store, no version, no detection: data enough to load it.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PluginIdentity {
24    /// The name the plugin reports (`pnpm`) -- checked against what it says about itself once loaded.
25    pub name: String,
26    /// Full crate name (`pmpx-plugin-pnpm`), for messages.
27    pub crate_name: String,
28    /// Its directory in the store.
29    pub dir: PathBuf,
30    /// The files its manifest declared, which it may ask to see.
31    pub wanted: Vec<String>,
32    /// The ABI the manifest declares. Diagnostics only: what the *plugin* reports is the authority,
33    /// and the loader has already refused one built against another major.
34    pub declared_abi: Option<u32>,
35}
36
37/// Everything one call needs that a plugin cannot work out for itself.
38///
39/// A borrowed view of the run's own state, assembled at the call site: the plugin reads it through the
40/// ABI, and nothing here is copied for the host's own benefit.
41pub struct Call<'a> {
42    /// Project root: where the command runs unless the answer names a `cwd` of its own.
43    pub root: &'a Path,
44    /// The directory the person ran pmpx from -- **not** where the command will run.
45    pub start_dir: &'a Path,
46    /// The files the plugin's own detection matched.
47    pub matched: &'a [String],
48    /// The `.pmpx.toml` files that were read, nearest first.
49    pub config_files: &'a [PathBuf],
50    /// `[plugin]` pins from the project config.
51    pub pins: &'a std::collections::BTreeMap<String, String>,
52    /// The arguments the user typed.
53    pub args: &'a [std::ffi::OsString],
54    /// Which verb this is.
55    pub verb: Verb,
56    /// Why this plugin was selected.
57    pub reason: SelectionReason,
58    /// The score it won with.
59    pub score: u32,
60    /// Reads the files the plugin declared, on demand -- and refuses everything else.
61    pub files: &'a Declared,
62}
63
64/// A plugin that is loaded and has passed the identity check.
65///
66/// It holds the library open, so every function pointer in its tables stays valid for as long as this
67/// value lives.
68pub struct Backend {
69    plugin: Plugin,
70    /// The plugin name declared in the manifest.
71    name: String,
72    /// The files the manifest declared, which the plugin may ask to see.
73    wanted: Vec<String>,
74}
75
76impl Backend {
77    /// Open the plugin's library, check what it says about itself, and hand it the host's hooks.
78    ///
79    /// `library` is the file the store found for this plugin; this crate does not search for it, because
80    /// only the store knows how a plugin's directory is laid out.
81    pub fn load(
82        identity: &PluginIdentity,
83        library: &Path,
84        levels: Levels,
85        events: &mut dyn FnMut(Event),
86    ) -> Result<Self> {
87        // SAFETY: the file comes from the plugin store, and loading a dynamic library runs whatever code
88        // is inside it -- that is the point of a plugin.
89        let loaded = unsafe { Plugin::open(library) }.map_err(|error| {
90            EngineError::NotFound(format!(
91                "cannot load {}\n{error}\n\
92                 The file is {} -- removing and installing the plugin again usually settles it.",
93                identity.crate_name,
94                library.display()
95            ))
96        })?;
97
98        // The self-reported name must match what the manifest declares. A plugin that panicked inside
99        // `name()` answers with the contract's marker instead, which would otherwise look like an odd
100        // pair of names -- so say what actually happened.
101        let self_reported = loaded.name();
102        if self_reported == pmpx_plugin::shell::PANIC_MARKER {
103            return Err(EngineError::NotFound(format!(
104                "plugin {} panicked while reporting its name -- its own panic message is on stderr \
105                 above.\nDelete {} and install it again.",
106                identity.name,
107                identity.dir.display()
108            )));
109        }
110
111        if self_reported != identity.name {
112            return Err(EngineError::NotFound(format!(
113                "the plugin calls itself \"{self_reported}\", but the manifest declares \"{}\" -- \
114                 refusing to load.\nDelete {} and install it again.",
115                identity.name,
116                identity.dir.display()
117            )));
118        }
119
120        // A manifest that disagrees with the library beside it means the wrapper and the plugin came
121        // from different builds, which is worth saying out loud -- it is what a stale install looks like.
122        if let Some(declared) = identity.declared_abi {
123            if declared != pmpx_plugin::abi::PMPX_ABI_MAJOR {
124                events(Event::Warning(format!(
125                    "plugin {} declares ABI {declared} in its manifest but its library reports {}",
126                    identity.name,
127                    pmpx_plugin::abi::PMPX_ABI_MAJOR
128                )));
129            }
130        }
131
132        // Everything is validated, so the plugin may now be told about its host. This is also what makes
133        // `pmpx_plugin::debug!` reach pmpx instead of falling back to the plugin's own stderr, and what
134        // keeps a plugin's notes from being formatted at all when nobody asked for them.
135        loaded.tables().attach(log::hooks(levels));
136
137        Ok(Self {
138            plugin: loaded,
139            name: identity.name.clone(),
140            wanted: identity.wanted.clone(),
141        })
142    }
143
144    /// The name the plugin reports for itself, which the manifest has already been checked against.
145    pub fn name(&self) -> &str {
146        &self.name
147    }
148
149    /// The family the plugin reports for itself.
150    pub fn family(&self) -> String {
151        self.plugin.family().to_string()
152    }
153
154    /// The rustc version and target the plugin was built with. Diagnostics only.
155    pub fn build_info(&self) -> (String, String) {
156        self.plugin.tables().build_info()
157    }
158
159    /// The files the manifest declared this plugin may be handed.
160    pub fn wanted_files(&self) -> &[String] {
161        &self.wanted
162    }
163
164    /// The host's hooks, for a caller that wants to attach them itself.
165    pub fn hooks(levels: Levels) -> &'static PmpxHost {
166        log::hooks(levels)
167    }
168
169    /// Ask the plugin to map one call to one command.
170    ///
171    /// A [`CallError`] is a *business* answer -- "I cannot do that" -- not a failure of the crossing
172    /// itself, which is why it is returned rather than raised. Everything the plugin said while it ran
173    /// (its `debug!` lines, what the file provider refused) reaches `events`.
174    pub fn command(
175        &self,
176        call: &Call<'_>,
177        events: &mut dyn FnMut(Event),
178    ) -> std::result::Result<CommandSpec, CallError> {
179        let source = ContextSource {
180            root: call.root,
181            start_dir: call.start_dir,
182            matched: call.matched,
183            config_files: call.config_files,
184            pins: call.pins,
185            args: call.args,
186            verb: call.verb.to_abi(),
187            reason: call.reason.to_abi(),
188            score: call.score,
189            files: call.files,
190        };
191
192        // A plugin's messages can never be attributed to the previous call: the queue starts empty.
193        log::clear();
194
195        let answer = self.plugin.tables().call(&source);
196
197        // The plugin's own lines first, then whatever the file provider had to say: both belong to this
198        // call, and both are drained exactly once.
199        log::drain(events);
200        for note in call.files.take_notes() {
201            events(Event::Note(note));
202        }
203
204        answer.map(|command| CommandSpec {
205            program: command.program,
206            args: command.args,
207            cwd: command.cwd,
208        })
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    /// The identity check is the one thing a manifest is for: a plugin that calls itself something else
217    /// is refused rather than used.
218    #[test]
219    fn an_identity_mismatch_is_a_setup_error() {
220        let error = EngineError::NotFound(format!(
221            "the plugin calls itself \"other\", but the manifest declares \"{}\" -- refusing to load.",
222            "wanted"
223        ));
224
225        assert!(error.is_not_found());
226        assert!(error.message().contains("wanted"));
227    }
228
229    /// The engine's view of a plugin is data: nothing about the store, a version, or a score.
230    #[test]
231    fn an_identity_is_data() {
232        let identity = PluginIdentity {
233            name: "pnpm".to_string(),
234            crate_name: "pmpx-plugin-pnpm".to_string(),
235            dir: PathBuf::from("/plugins/pnpm"),
236            wanted: vec!["package.json".to_string()],
237            declared_abi: Some(3),
238        };
239
240        assert_eq!(identity.wanted.len(), 1);
241        assert_eq!(identity.name, "pnpm");
242    }
243}