use std::ffi::OsString;
use std::path::Path;
use pmpx_plugin::Verb;
use crate::error::Result;
use crate::files::Declared;
use crate::session::Session;
use crate::ChildOutput;
use crate::{detect, Call, EngineError, Event, Plan};
pub fn run_verb(
session: &Session,
verb: Verb,
args: &[OsString],
allow_exec_fallback: bool,
events: &mut dyn FnMut(Event),
) -> Result<u8> {
let root = match session.project_root() {
Some(root) => root.to_path_buf(),
None => {
if allow_exec_fallback {
return passthrough(
&session.start_dir.clone(),
args,
session.child_output(),
events,
);
}
return Err(session.no_project_error());
}
};
let selection = match session.select(&root, events) {
Ok(selection) => selection,
Err(failure) => {
if allow_exec_fallback {
return passthrough(&root, args, session.child_output(), events);
}
return Err(EngineError::Detect(failure));
}
};
events(Event::Notes {
plugin: selection.name.clone(),
notes: selection.notes.clone(),
});
let backend = session.load_backend(&selection, events)?;
let files = Declared::new(&root, backend.wanted_files());
let call = Call {
root: &root,
start_dir: &session.start_dir,
matched: &selection.matched,
config_files: &session.project.sources,
pins: &session.project.plugin,
args,
verb,
reason: detect::reason_of(selection.reason),
score: selection.score,
files: &files,
};
match backend.command(&call, events) {
Ok(answer) => {
let plan = plan_of(answer);
crate::run(&plan, &root, session.child_output(), events)
}
Err(pmpx_loader::CallError::UnsupportedVerb) if allow_exec_fallback => {
passthrough(&root, args, session.child_output(), events)
}
Err(error) => Err(EngineError::Call {
plugin: selection.name.clone(),
verb: verb.to_string(),
error,
}),
}
}
pub fn run_script(
session: &Session,
name: &str,
extra: &[OsString],
events: &mut dyn FnMut(Event),
) -> Option<Result<u8>> {
let script = session.project.scripts.get(name)?;
let mut tokens = script.tokens();
if tokens.is_empty() {
return Some(Err(EngineError::Usage(format!(
"the [scripts] entry \"{name}\" is empty, so there is nothing to run"
))));
}
let plan = Plan::new(OsString::from(tokens.remove(0)))
.args(tokens)
.args(extra.iter());
let cwd = session
.project_root()
.map(Path::to_path_buf)
.unwrap_or_else(|| session.start_dir.clone());
Some(crate::run(&plan, &cwd, session.child_output(), events))
}
fn passthrough(
cwd: &Path,
args: &[OsString],
child_output: ChildOutput,
events: &mut dyn FnMut(Event),
) -> Result<u8> {
let Some((program, rest)) = args.split_first() else {
return Err(EngineError::Usage(
"nothing to run: `pmpx exec` needs a command".to_string(),
));
};
let plan = Plan::new(program.clone()).args(rest.iter());
crate::run(&plan, cwd, child_output, events)
}
fn plan_of(answer: pmpx_plugin::CommandSpec) -> Plan {
Plan {
program: answer.program,
args: answer.args,
cwd: answer.cwd,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_answer_becomes_a_plan_unchanged() {
let answer = pmpx_plugin::CommandSpec::new("cargo")
.arg("add")
.arg("serde")
.cwd("/work/sub");
let plan = plan_of(answer);
assert_eq!(plan.program, OsString::from("cargo"));
assert_eq!(
plan.args,
vec![OsString::from("add"), OsString::from("serde")]
);
assert_eq!(plan.cwd, Some(std::path::PathBuf::from("/work/sub")));
assert_eq!(plan.display(), "cargo add serde");
}
#[test]
fn an_empty_passthrough_is_a_usage_error() {
let error =
passthrough(Path::new("/tmp"), &[], ChildOutput::Inherit, &mut |_| {}).unwrap_err();
assert!(matches!(error, EngineError::Usage(_)), "{error:?}");
}
}