use pmpx_plugin::Family;
use std::path::{Path, PathBuf};
use crate::app;
use crate::cli::Cli;
use crate::error::{PmpxError, EXIT_OK};
use crate::style;
pub(super) fn plugin_add(
args: &Cli,
names: &[String],
version: Option<&str>,
) -> crate::error::Result<u8> {
let session = app::session(args)?;
if version.is_some() && names.len() > 1 {
return Err(PmpxError::Usage(
"--version can only be used with a single plugin name".to_string(),
));
}
let mut installed_so_far: Vec<String> = Vec::new();
for name in names {
if let Some(dir) = checkout_of(name, session.manifest_name()) {
anstream::println!("Installing {}...", style::paint(style::PM, name));
match install_checkout(&session, &dir) {
Ok(installed) => {
anstream::println!(
" {} v{} ({}) -> {}",
installed.crate_name,
style::paint(style::DIM, installed.version),
style::paint(style::DIM, describe_source(&installed.source)),
style::paint(style::DIM, installed.dir.display())
);
installed_so_far.push(installed.crate_name);
}
Err(e) => {
if !installed_so_far.is_empty() {
crate::error::note_line(format!(
"{name} was not installed; installed before it: {}",
installed_so_far.join(", ")
));
}
return Err(e);
}
}
continue;
}
anstream::println!("Installing {}...", style::paint(style::PM, name));
match session.install(name, version) {
Ok(installed) => {
anstream::println!(
" {} v{} ({}) -> {}",
installed.crate_name,
style::paint(style::DIM, installed.version),
style::paint(style::DIM, describe_source(&installed.source)),
style::paint(style::DIM, installed.dir.display())
);
installed_so_far.push(installed.crate_name);
}
Err(e) => {
if !installed_so_far.is_empty() {
crate::error::note_line(format!(
"{name} was not installed; installed before it: {}",
installed_so_far.join(", ")
));
}
return Err(e.into());
}
}
}
Ok(EXIT_OK)
}
fn checkout_of(name: &str, manifest: &str) -> Option<PathBuf> {
let path = Path::new(name);
if path.join(manifest).is_file() {
return Some(path.to_path_buf());
}
let spelled_like_a_path = name == "."
|| name == ".."
|| name.contains('/')
|| name.contains('\\')
|| path.is_absolute();
if spelled_like_a_path && path.is_dir() {
return Some(path.to_path_buf());
}
None
}
fn install_checkout(
session: &app::Session,
dir: &Path,
) -> crate::error::Result<pmpx_engine::store::Installed> {
if !dir.is_dir() {
return Err(PmpxError::Usage(format!(
"{} is not a directory, so there is no plugin to install from it",
dir.display()
)));
}
vet_checkout(session, dir)?;
Ok(session.install_from_path(dir)?)
}
fn vet_checkout(session: &app::Session, dir: &Path) -> crate::error::Result<()> {
let report = pmpx_engine::store::vet_checkout(dir, session.manifest_name())?;
match report.abi {
Some(abi) if abi == pmpx_plugin::abi::PMPX_ABI_MAJOR => {}
Some(abi) => {
return Err(PmpxError::Usage(format!(
"{} declares ABI {abi}, but this pmpx speaks ABI {}.\n\
Rebuild the plugin against pmpx-plugin {}.",
dir.display(),
pmpx_plugin::abi::PMPX_ABI_MAJOR,
env!("CARGO_PKG_VERSION")
)))
}
None => {
return Err(PmpxError::Usage(format!(
"{} does not declare an abi, so pmpx cannot tell whether it can load it",
dir.display()
)))
}
}
if report.family.as_deref().unwrap_or("").is_empty() {
crate::error::note_line(format!(
"{} declares no family, so no project can ever select it",
dir.display()
));
}
if report.markers.is_empty() {
crate::error::note_line(format!(
"{} declares no [detect] markers, so it will never match a project",
dir.display()
));
}
Ok(())
}
pub(super) fn plugin_rm(args: &Cli, names: &[String]) -> crate::error::Result<u8> {
let session = app::session(args)?;
for name in names {
session.uninstall(name)?;
anstream::println!("Removed {}", style::paint(style::PM, name));
}
Ok(EXIT_OK)
}
pub(super) fn plugin_update(
args: &Cli,
names: &[String],
version: Option<&str>,
) -> crate::error::Result<u8> {
let session = app::session(args)?;
if version.is_some() && names.len() > 1 {
return Err(PmpxError::Usage(
"--version can only be used with a single plugin name".to_string(),
));
}
let targets: Vec<String> = if names.is_empty() {
session
.plugins
.usable()
.map(|p| p.name().to_string())
.collect()
} else {
names.to_vec()
};
if targets.is_empty() {
anstream::println!("No plugins to update.");
return Ok(EXIT_OK);
}
for name in targets {
let installed = session.update(&name, version)?;
anstream::println!(
"{} -> v{}",
installed.crate_name,
style::paint(style::DIM, installed.version)
);
}
Ok(EXIT_OK)
}
pub(super) fn plugin_search(args: &Cli, keyword: &str, limit: usize) -> crate::error::Result<u8> {
let session = app::session(args)?;
let results = session.search(keyword, limit)?;
if results.is_empty() {
anstream::println!("crates.io has no crate matching {keyword:?}.");
return Ok(EXIT_OK);
}
for r in results {
anstream::println!(
"{:<32} v{} ↓{}",
r.name,
style::padded(style::DIM, 10, &r.version),
r.downloads
);
if let Some(d) = r.description {
anstream::println!(" {}", style::paint(style::DIM, d));
}
}
Ok(EXIT_OK)
}
pub(super) fn plugin_info(args: &Cli, name: &str) -> crate::error::Result<u8> {
let session = app::session(args)?;
let mut found = false;
if let Some(p) = session.plugins.by_name(name) {
found = true;
anstream::println!("Installed");
anstream::println!(
" {} {}",
style::label(15, "reported name"),
style::paint(style::PM, &p.name())
);
anstream::println!(" {} {}", style::label(15, "crate"), p.crate_name());
anstream::println!(
" {} {}",
style::label(15, "version"),
style::paint(style::DIM, &p.version())
);
anstream::println!(
" {} {}",
style::label(15, "family"),
p.family
.as_ref()
.map(Family::as_str)
.unwrap_or("(not declared)")
);
anstream::println!(
" {} {}",
style::label(15, "ABI"),
p.abi()
.map(|a| a.to_string())
.unwrap_or_else(|| "(not declared)".into())
);
anstream::println!(
" {} {}",
style::label(15, "directory"),
style::paint(style::DIM, p.dir().display())
);
anstream::println!(
" {} {}",
style::label(15, "strong evidence"),
style::paint(style::DIM, join_or_dash(&p.strong))
);
anstream::println!(
" {} {}",
style::label(15, "weak evidence"),
style::paint(style::DIM, join_or_dash(&p.weak))
);
if let Some(why) = p.problem() {
anstream::println!(" {}", style::paint(style::DIM, format!("⚠ {why}")));
}
anstream::println!();
if let Ok(backend) = app::load_plugin(&session, p) {
let (rustc_version, target) = backend.build_info();
anstream::println!("Plugin reports");
anstream::println!(
" {} {}",
style::label(15, "name"),
style::paint(style::PM, backend.name())
);
anstream::println!(" {} {}", style::label(15, "family"), backend.family());
anstream::println!(
" {} {}",
style::label(15, "compiled with"),
style::paint(style::DIM, rustc_version)
);
anstream::println!(
" {} {}",
style::label(15, "target"),
style::paint(style::DIM, target)
);
anstream::println!();
}
}
let crate_name = session.normalize_crate_name(name);
match session.view(&crate_name) {
Ok(Some(info)) => {
found = true;
anstream::println!("crates.io");
anstream::println!(" {} {}", style::label(15, "crate"), info.name);
anstream::println!(
" {} {}",
style::label(15, "latest"),
style::paint(style::DIM, info.version)
);
if let Some(d) = info.description {
anstream::println!(
" {} {}",
style::label(15, "description"),
style::paint(style::DIM, d)
);
}
if let Some(r) = info.repository {
anstream::println!(
" {} {}",
style::label(15, "repository"),
style::paint(style::DIM, r)
);
}
}
Ok(None) => {
if !found {
anstream::println!("crates.io has no {crate_name}.");
}
}
Err(e) => {
crate::error::error_line(format!("crates.io lookup failed: {e}"));
}
}
if !found {
return Err(PmpxError::not_found(format!("no plugin named {name}.")));
}
Ok(EXIT_OK)
}
pub(super) fn join_or_dash(items: &[String]) -> String {
if items.is_empty() {
"(none)".to_string()
} else {
items.join(", ")
}
}
pub(super) fn describe_source(source: &pmpx_engine::store::InstallSource) -> String {
use pmpx_engine::store::InstallSource;
match source {
InstallSource::Prebuilt => "prebuilt".to_string(),
InstallSource::BuildHost => "built locally".to_string(),
InstallSource::Local { path } => format!("built from {}", path.display()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn join_or_dash_handles_the_empty_case() {
assert_eq!(join_or_dash(&[]), "(none)");
assert_eq!(join_or_dash(&["a".into(), "b".into()]), "a, b");
}
}