use std::cell::Cell;
use std::path::{Path, PathBuf};
use std::time::Instant;
use crate_plugin_kit::{CratePluginKit, KitConfig};
use pmpx_detect::{DetectFailure, FamilyScore, Selection};
use pmpx_plugin::abi::PmpxPlugin;
use pmpx_project::{GlobalConfig, MergedProjectConfig};
use crate::discovery::{self, StopReason, Walk};
use crate::error::Result;
use crate::log::Levels;
use crate::store::{InstalledPlugin, PluginSet};
use crate::ChildOutput;
use crate::{EngineError, Event};
#[derive(Debug, Clone, Default)]
pub struct Options {
pub start_dir: Option<PathBuf>,
pub no_walk_up: bool,
pub wanted_plugin: Option<String>,
pub quiet: bool,
pub trace: bool,
pub child_output_on_stderr: bool,
pub contract_version: String,
}
pub struct Session {
pub start_dir: PathBuf,
pub global: GlobalConfig,
pub project: MergedProjectConfig,
pub plugins: PluginSet,
pub(crate) kit: CratePluginKit<PmpxPlugin>,
pub walk: Walk,
pub project_root: Option<PathBuf>,
pub wanted_plugin: Option<String>,
pub quiet: bool,
trace: bool,
pub(crate) child_output_on_stderr: bool,
}
impl Session {
pub fn open(options: Options, events: &mut dyn FnMut(Event)) -> Result<Self> {
let start_dir = phase(
events,
"session.start-dir",
|| resolve_start_dir(options.start_dir.as_deref()),
|dir| dir.display().to_string(),
)?;
let global = phase(
events,
"session.config",
|| {
GlobalConfig::load().map_err(|error| {
EngineError::Setup(format!("failed to read the config: {error}"))
})
},
|_| "global config.toml".to_string(),
)?;
let mut discovery_cfg = global.discovery.clone();
if options.no_walk_up {
discovery_cfg.walk_up = false;
}
let data_dir = global.plugin_store.effective_data_dir().map_err(|error| {
EngineError::Setup(format!("cannot decide the plugin directory: {error}"))
})?;
let mut kit_cfg = KitConfig::new("pmpx").with_data_dir(&data_dir);
kit_cfg.contract_version = options.contract_version;
kit_cfg.entry_symbol = pmpx_plugin::abi::PMPX_ENTRY_SYMBOL.as_bytes().to_vec();
kit_cfg.prefer_prebuilt = global.plugin_store.effective_prefer_prebuilt();
let kit = phase(
events,
"session.store",
|| {
CratePluginKit::<PmpxPlugin>::new(kit_cfg).map_err(|error| {
EngineError::Setup(format!(
"failed to initialise the plugin store: {}\n{error}",
data_dir.display()
))
})
},
|_| data_dir.display().to_string(),
)?;
let plugins = phase(
events,
"session.plugins",
|| {
PluginSet::load(&kit).map_err(|error| {
EngineError::Setup(format!("failed to read the installed plugins: {error}"))
})
},
|set| {
format!(
"{} installed, {} usable",
set.plugins.len(),
set.usable().count()
)
},
)?;
let walk = phase(
events,
"session.walk",
|| Ok(discovery::walk(&start_dir, &discovery_cfg)),
|walk| {
format!(
"{}, stopped because: {}",
discovery::dirs(walk.dirs.len()),
walk.stopped.describe(discovery_cfg.max_depth)
)
},
)?;
let checked = Cell::new(0usize);
let project_root = phase(
events,
"session.root",
|| {
Ok(walk.project_root(|dir| {
checked.set(checked.get() + 1);
plugins.marks_root(dir)
}))
},
|root| match root {
Some(root) => format!("{} checked -> {}", checked.get(), root.display()),
None => format!("{} checked -> none", checked.get()),
},
)?;
let project = phase(
events,
"session.project-conf",
|| {
MergedProjectConfig::from_paths_near_to_far(&walk.config_paths()).map_err(|error| {
EngineError::Setup(format!("failed to read the project config: {error}"))
})
},
|project| format!("{} .pmpx.toml", project.sources.len()),
)?;
Ok(Self {
start_dir,
global,
project,
plugins,
kit,
walk,
project_root,
wanted_plugin: options.wanted_plugin,
quiet: options.quiet,
trace: options.trace,
child_output_on_stderr: options.child_output_on_stderr,
})
}
pub fn project_root(&self) -> Option<&Path> {
self.project_root.as_deref()
}
pub fn no_project_error(&self) -> EngineError {
let mut msg = format!("no project type detected in {}.", self.start_dir.display());
msg.push_str(&format!(
"\nWalked up {}, stopped because: {}.",
discovery::dirs(self.walk.dirs.len()),
self.walk.stopped.describe(self.global.discovery.max_depth)
));
if self.walk.stopped == StopReason::WalkUpDisabled {
msg.push_str(
"\n(`--no-walk-up` or the global `[discovery] walk_up = false` turned walk-up off)",
);
}
let installed: Vec<String> = self
.plugins
.usable()
.map(|p| p.name().to_string())
.collect();
if installed.is_empty() {
msg.push_str(
"\nNo plugins are installed. pmpx decides the project type from the marker files \
declared by installed plugins, so nothing can be detected right now.",
);
} else {
msg.push_str(&format!("\nInstalled plugins: {}", installed.join(", ")));
let mut markers: Vec<&str> = self
.plugins
.detect_names()
.iter()
.map(String::as_str)
.collect();
markers.sort_unstable();
markers.dedup();
msg.push_str(&format!(
"\nThey look for these files: {}",
markers.join(", ")
));
}
msg.push_str(
"\n\nYou can declare it explicitly with a .pmpx.toml in the current directory, for \
example:\n [plugin]\n rust = \"cargo\"",
);
EngineError::NoProject(msg)
}
pub fn levels(&self) -> Levels {
Levels {
quiet: self.quiet,
trace: self.trace,
}
}
pub fn select(
&self,
root: &Path,
events: &mut dyn FnMut(Event),
) -> std::result::Result<Selection, DetectFailure> {
crate::detect::select(
&self.plugins,
root,
&self.project,
&self.global,
self.wanted_plugin.as_deref(),
events,
)
}
pub fn select_from(
&self,
root: &Path,
families: &std::collections::BTreeMap<String, FamilyScore>,
events: &mut dyn FnMut(Event),
) -> std::result::Result<Selection, DetectFailure> {
crate::detect::select_from_scores(
&self.plugins,
root,
families,
&self.project,
&self.global,
self.wanted_plugin.as_deref(),
events,
)
}
pub fn load_backend(
&self,
selection: &Selection,
events: &mut dyn FnMut(Event),
) -> Result<crate::Backend> {
let Some(plugin) = self.plugins.by_crate_name(&selection.crate_name) else {
return Err(EngineError::Setup(format!(
"plugin {} is not in the list (deleted between the two reads?)",
selection.crate_name
)));
};
let started = Instant::now();
let loaded = self.load_plugin(plugin, events);
events(Event::Phase {
name: "backend.load",
micros: started.elapsed().as_micros(),
detail: match &loaded {
Ok(_) => plugin.crate_name().to_string(),
Err(error) => format!("{} FAILED: {error}", plugin.crate_name()),
},
});
loaded
}
pub fn load_plugin(
&self,
plugin: &InstalledPlugin,
events: &mut dyn FnMut(Event),
) -> Result<crate::Backend> {
let identity = crate::PluginIdentity {
name: plugin.name().to_string(),
crate_name: plugin.crate_name().to_string(),
dir: plugin.dir().to_path_buf(),
wanted: plugin.wanted.clone(),
declared_abi: plugin.abi(),
};
let stem = plugin.crate_name().replace('-', "_");
let library = crate_plugin_kit::find_library(plugin.dir(), &stem).map_err(|error| {
EngineError::Setup(format!(
"cannot find the plugin library for {} in {}: {error}",
plugin.crate_name(),
plugin.dir().display()
))
})?;
crate::Backend::load(&identity, &library, self.levels(), events)
}
}
fn phase<T, D>(
events: &mut dyn FnMut(Event),
name: &'static str,
f: impl FnOnce() -> Result<T>,
describe: D,
) -> Result<T>
where
D: FnOnce(&T) -> String,
{
let started = Instant::now();
let out = f();
events(Event::Phase {
name,
micros: started.elapsed().as_micros(),
detail: match &out {
Ok(value) => describe(value),
Err(error) => error.message(),
},
});
out
}
fn resolve_start_dir(dir: Option<&Path>) -> Result<PathBuf> {
match dir {
Some(dir) => {
if !dir.is_dir() {
return Err(EngineError::Usage(format!(
"cannot use {} as the directory: it is not a directory",
dir.display()
)));
}
Ok(discovery::normalize(dir))
}
None => std::env::current_dir().map_err(|error| {
EngineError::Setup(format!("cannot read the current directory: {error}"))
}),
}
}
impl Session {
pub fn manifest_name(&self) -> &str {
&self.kit.config().manifest_name
}
pub fn normalize_crate_name(&self, name: &str) -> String {
self.kit.config().normalize_crate_name(name)
}
pub fn install(&self, name: &str, version: Option<&str>) -> Result<crate::store::Installed> {
Ok(self.kit.install(name, version)?)
}
pub fn install_from_path(&self, dir: &Path) -> Result<crate::store::Installed> {
Ok(self.kit.install_from_path(dir)?)
}
pub fn uninstall(&self, name: &str) -> Result<()> {
Ok(self.kit.uninstall(name)?)
}
pub fn update(&self, name: &str, version: Option<&str>) -> Result<crate::store::Installed> {
Ok(self.kit.update(name, version)?)
}
pub fn search(&self, keyword: &str, limit: usize) -> Result<Vec<crate::store::CrateSummary>> {
Ok(self.kit.search(keyword, limit)?)
}
pub fn view(&self, crate_name: &str) -> Result<Option<crate::store::CrateInfo>> {
Ok(self.kit.view(crate_name)?)
}
}
impl Session {
pub(crate) fn child_output(&self) -> ChildOutput {
if self.child_output_on_stderr {
ChildOutput::OnStderr
} else {
ChildOutput::Inherit
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_start_dir_rejects_a_missing_directory() {
let error = resolve_start_dir(Some(Path::new("definitely/not/here"))).unwrap_err();
assert!(
error.message().contains("definitely"),
"{}",
error.message()
);
}
#[test]
fn resolve_start_dir_accepts_an_existing_one() {
let dir = std::env::temp_dir().join(format!("pmpx-start-dir-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let resolved = resolve_start_dir(Some(&dir)).unwrap();
assert!(resolved.is_absolute());
assert_eq!(
resolved.file_name(),
dir.file_name(),
"a plain path stays the path it was: {resolved:?}"
);
assert!(
!resolved.to_string_lossy().starts_with(r"\\?\"),
"pmpx must never introduce a Windows verbatim path: {resolved:?}"
);
std::fs::remove_dir_all(&dir).unwrap();
}
}