use std::path::PathBuf;
use std::process::ExitCode;
use dre_core::lock::Lock;
use dre_core::manager::{self, Index, IndexPackage};
use dre_core::project::{self, LoadOptions, PluginSource, Project};
use semver::VersionReq;
use crate::output::{Printer, Tone};
pub fn ensure(project: &Project, install: bool, resolve: bool, printer: &Printer) -> bool {
match manager::sync(project, install, resolve, |m| {
printer.line(Tone::Note, "Installed", m.trim_start_matches("installed "))
}) {
Ok(_) => true,
Err(errors) => {
for e in errors {
printer.error(&e);
}
false
}
}
}
pub fn sync_packages(root: &std::path::Path, printer: &Printer) -> bool {
match dre_core::packages::sync(root, true, |m| printer.line(Tone::Note, "Installed", m)) {
Ok(()) => true,
Err(errors) => {
for e in errors {
printer.error(&e);
}
false
}
}
}
pub fn check_for_validate(
project: &Project,
install: bool,
diags: &mut dre_core::Diagnostics,
printer: &Printer,
) {
if diags.has_errors() {
return;
}
let r = manager::sync(project, install, false, |m| {
printer.line(Tone::Note, "Installed", m.trim_start_matches("installed "))
});
if let Err(errors) = r {
for e in errors {
if install {
diags.error("plugin-install-failed", None, None, e);
} else {
diags.warning("plugin-not-installed", None, None, e);
}
}
}
}
fn parse(spec: &str) -> Result<(String, Option<VersionReq>), String> {
match spec.split_once('@') {
Some((i, r)) => Ok((
i.to_string(),
Some(VersionReq::parse(r).map_err(|e| format!("invalid version `{r}`: {e}"))?),
)),
None => Ok((spec.to_string(), None)),
}
}
fn project_at(dir: &std::path::Path) -> Option<Project> {
if !dir.join(project::PROJECT_FILE).is_file() {
return None;
}
project::load(dir, &LoadOptions::default()).0
}
fn package<'a>(index: &'a Index, name: &str) -> Result<&'a IndexPackage, String> {
if let Some(p) = index.package(name) {
return Ok(p);
}
let inside: Vec<String> = index
.providers_of_name(name)
.iter()
.map(|p| format!("`{}`", p.name))
.collect();
Err(if inside.is_empty() {
format!("the registry has no plugin package `{name}`")
} else {
format!(
"the registry has no plugin package `{name}`; the `{name}` plugin comes in {}",
inside.join(", ")
)
})
}
pub fn install(spec: String, project_dir: PathBuf, update: bool, printer: &Printer) -> ExitCode {
let (name, cli_req) = match parse(&spec) {
Ok(x) => x,
Err(e) => {
printer.error(&e);
return ExitCode::FAILURE;
}
};
let project = project_at(&project_dir);
let declared = project
.iter()
.flat_map(|p| &p.plugins)
.find(|r| r.name == name)
.cloned();
let source = declared.as_ref().map(|r| r.source.clone()).unwrap_or_default();
if let PluginSource::Local(path) = &source {
printer.line(
Tone::Note,
"Local",
&format!("plugin package `{name}` is used from {path}; there's nothing to install"),
);
return ExitCode::SUCCESS;
}
let index = match Index::for_source(&source, &name) {
Ok(i) => i,
Err(e) => {
printer.error(&e);
return ExitCode::FAILURE;
}
};
let package = match package(&index, &name) {
Ok(p) => p,
Err(e) => {
printer.error(&e);
return ExitCode::FAILURE;
}
};
let source_key = source.lock_key();
let declared_req = declared.as_ref().map(|r| r.req());
let mut lock = project.as_ref().map(|p| Lock::load(&p.root).unwrap_or_default());
let req = match (&declared_req, &cli_req) {
(Some(d), Some(c)) => {
if !dre_core::constraints::compatible(&[d, c]) {
printer.error(&format!(
"`{c}` contradicts the project's declared constraint `{d}` for `{name}`"
));
return ExitCode::FAILURE;
}
dre_core::constraints::combine(&[d, c])
}
(Some(d), None) => d.clone(),
(None, Some(c)) => c.clone(),
(None, None) => VersionReq::STAR,
};
let pinned = lock
.as_ref()
.and_then(|l| l.get(&name).cloned())
.filter(|l| l.from == source_key);
let version = match (&pinned, update, &cli_req) {
(Some(l), false, None) => package.exact(&l.version),
_ => package.best(&req),
};
let Some(version) = version else {
printer.error(&format!(
"no version of the plugin package `{name}` matches `{req}` for {}",
manager::platform()
));
return ExitCode::FAILURE;
};
let dir = dre_core::plugins::plugins_dir(project.as_ref().map(|p| p.root.as_path()));
let installed = if source.is_default() {
manager::install_linked(&dir, package, version, None)
} else {
manager::install(&dir, package, version, None)
};
match installed {
Ok(mut locked) => {
locked.from = source_key.clone();
let provides: Vec<String> = locked.provides.iter().map(|p| p.to_string()).collect();
printer.line(
Tone::Good,
"Installed",
&format!(
"plugin package `{name}` {} ({})",
locked.version,
provides.join(", ")
),
);
if let (Some(p), Some(l)) = (&project, lock.as_mut())
&& declared.is_some()
{
l.plugins.insert(name.clone(), locked);
if let Err(e) = l.save(&p.root) {
printer.error(&e);
return ExitCode::FAILURE;
}
printer.line(
Tone::Note,
"Locked",
&format!("dre.lock pins `{name}` to {}", version.version),
);
}
ExitCode::SUCCESS
}
Err(e) => {
printer.error(&e);
ExitCode::FAILURE
}
}
}
pub fn remove(spec: String, project_dir: PathBuf, printer: &Printer) -> ExitCode {
let (name, version) = match spec.split_once('@') {
Some((i, v)) => match semver::Version::parse(v) {
Ok(v) => (i.to_string(), Some(v)),
Err(e) => {
printer.error(&format!("`{v}` isn't an exact version: {e}"));
return ExitCode::FAILURE;
}
},
None => (spec.clone(), None),
};
let in_project = project_dir.join(project::PROJECT_FILE).is_file();
let dir = dre_core::plugins::plugins_dir(in_project.then_some(project_dir.as_path()));
let targets: Vec<_> = dre_core::plugins::discover(&dir)
.into_iter()
.filter(|p| p.name == name)
.filter(|p| version.is_none() || p.version == version)
.collect();
if targets.is_empty() {
printer.error(&format!("no installed plugin package matches `{spec}`"));
return ExitCode::FAILURE;
}
for t in &targets {
let r = match &t.version {
Some(_) => t.path.parent().map_or(Ok(()), std::fs::remove_dir_all),
None => std::fs::remove_file(&t.path),
};
if let Err(e) = r {
printer.error(&format!("can't remove {}: {e}", t.path.display()));
return ExitCode::FAILURE;
}
let v = t
.version
.as_ref()
.map(|v| v.to_string())
.unwrap_or_else(|| "(unversioned)".into());
printer.line(Tone::Good, "Removed", &format!("plugin package `{}` {v}", t.name));
}
if let Some(p) = project_at(&project_dir)
&& let Ok(mut lock) = Lock::load(&p.root)
{
let gone = lock.get(&name).is_some_and(|l| {
targets
.iter()
.any(|t| t.version.is_none() || t.version.as_ref() == Some(&l.version))
});
if gone {
lock.plugins.remove(&name);
if let Err(e) = lock.save(&p.root) {
printer.error(&e);
return ExitCode::FAILURE;
}
printer.line(Tone::Note, "Unlocked", "removed the pin from dre.lock");
}
}
ExitCode::SUCCESS
}