use thiserror::Error;
use crate::cargo::{self, Cargo};
use crate::detect::{detect_all, pick_by_name};
use crate::manager::{PackageError, PackageManager, Runner};
#[derive(Debug, Clone, Default)]
pub struct DepGroup {
pub group: String,
pub pacman: Vec<String>,
pub apt: Vec<String>,
pub dnf: Vec<String>,
pub brew: Vec<String>,
pub scoop: Vec<String>,
pub winget: Vec<String>,
}
#[derive(Debug, Error)]
pub enum DepsError {
#[error("no package manager detected; install one or use --manager")]
NoManagerDetected,
#[error("unknown package manager: {0}")]
UnknownManager(String),
#[error("install error: {0}")]
Install(#[from] PackageError),
}
pub struct DepsOpts {
pub groups: Vec<DepGroup>,
pub manager: Option<String>,
pub group_filter: Option<String>,
pub dry_run: bool,
}
pub struct DepsReport {
pub managers_used: Vec<String>,
pub installed: Vec<String>,
pub already_installed: Vec<String>,
pub skipped_unavailable: Vec<String>,
pub failed: Vec<(String, String)>,
}
pub struct CheckReport {
pub managers_used: Vec<String>,
pub found: Vec<String>,
pub missing: Vec<String>,
pub skipped_unavailable: Vec<String>,
pub failed: Vec<(String, String)>,
}
fn packages_for<'a>(group: &'a DepGroup, manager_name: &str) -> &'a [String] {
match manager_name {
"pacman" => &group.pacman,
"apt" => &group.apt,
"dnf" => &group.dnf,
"brew" => &group.brew,
"scoop" => &group.scoop,
"winget" => &group.winget,
_ => &[],
}
}
fn candidate_managers(opts: &DepsOpts) -> Result<Vec<Box<dyn PackageManager>>, DepsError> {
match &opts.manager {
Some(name) => pick_by_name(name)
.map(|m| vec![m])
.ok_or_else(|| DepsError::UnknownManager(name.clone())),
None => {
let detected = detect_all();
if detected.is_empty() {
Err(DepsError::NoManagerDetected)
} else {
Ok(detected)
}
}
}
}
struct GroupPlan<'a> {
manager: &'a dyn PackageManager,
native: Vec<(&'a str, &'a str)>,
cargo: Vec<(&'a str, &'a str)>,
}
fn plan_groups<'a>(
opts: &'a DepsOpts,
candidates: &'a [Box<dyn PackageManager>],
) -> (Vec<GroupPlan<'a>>, Vec<String>) {
let mut plans = Vec::new();
let mut unavailable = Vec::new();
for group in &opts.groups {
if opts
.group_filter
.as_deref()
.is_some_and(|f| f != group.group)
{
continue;
}
let Some((manager, pkgs)) = candidates.iter().find_map(|m| {
let pkgs = packages_for(group, m.name());
(!pkgs.is_empty()).then_some((m.as_ref(), pkgs))
}) else {
unavailable.push(group.group.clone());
continue;
};
let mut plan = GroupPlan {
manager,
native: Vec::new(),
cargo: Vec::new(),
};
for entry in pkgs {
match entry.strip_prefix(cargo::PREFIX) {
Some(krate) => plan.cargo.push((entry.as_str(), krate)),
None => plan.native.push((entry.as_str(), entry.as_str())),
}
}
plans.push(plan);
}
(plans, unavailable)
}
fn note_manager(used: &mut Vec<String>, manager: &dyn PackageManager) {
if !used.iter().any(|m| m == manager.name()) {
used.push(manager.name().to_owned());
}
}
fn sources<'p, 'a>(
plan: &'p GroupPlan<'a>,
) -> impl Iterator<Item = (&'a dyn PackageManager, &'p [(&'a str, &'a str)])> {
[
(plan.manager, plan.native.as_slice()),
(&Cargo as &dyn PackageManager, plan.cargo.as_slice()),
]
.into_iter()
.filter(|(_, entries)| !entries.is_empty())
}
pub fn install_deps(opts: &DepsOpts, runner: &dyn Runner) -> Result<DepsReport, DepsError> {
let candidates = candidate_managers(opts)?;
let (plans, skipped_unavailable) = plan_groups(opts, &candidates);
let mut report = DepsReport {
managers_used: Vec::new(),
installed: Vec::new(),
already_installed: Vec::new(),
skipped_unavailable,
failed: Vec::new(),
};
for plan in &plans {
for (manager, entries) in sources(plan) {
note_manager(&mut report.managers_used, manager);
if opts.dry_run {
report
.installed
.extend(entries.iter().map(|(written, _)| (*written).to_owned()));
continue;
}
let mut to_install: Vec<(&str, &str)> = Vec::new();
for &(written, name) in entries {
match manager.is_installed(runner, name) {
Ok(true) => report.already_installed.push(written.to_owned()),
Ok(false) => to_install.push((written, name)),
Err(e) => report.failed.push((written.to_owned(), e.to_string())),
}
}
if to_install.is_empty() {
continue;
}
let names: Vec<String> = to_install.iter().map(|(_, n)| (*n).to_owned()).collect();
match manager.install(runner, &names) {
Ok(()) => report
.installed
.extend(to_install.iter().map(|(w, _)| (*w).to_owned())),
Err(e) => {
let msg = e.to_string();
report.failed.extend(
to_install
.iter()
.map(|(w, _)| ((*w).to_owned(), msg.clone())),
);
}
}
}
}
Ok(report)
}
pub fn check_deps(opts: &DepsOpts, runner: &dyn Runner) -> Result<CheckReport, DepsError> {
let candidates = candidate_managers(opts)?;
let (plans, skipped_unavailable) = plan_groups(opts, &candidates);
let mut report = CheckReport {
managers_used: Vec::new(),
found: Vec::new(),
missing: Vec::new(),
skipped_unavailable,
failed: Vec::new(),
};
for plan in &plans {
for (manager, entries) in sources(plan) {
note_manager(&mut report.managers_used, manager);
for &(written, name) in entries {
match manager.exists(runner, name) {
Ok(true) => report.found.push(written.to_owned()),
Ok(false) => report.missing.push(written.to_owned()),
Err(e) => report.failed.push((written.to_owned(), e.to_string())),
}
}
}
}
Ok(report)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scoop::Scoop;
use crate::winget::Winget;
fn group(name: &str, scoop: &[&str], winget: &[&str]) -> DepGroup {
DepGroup {
group: name.into(),
scoop: scoop.iter().map(|p| (*p).to_owned()).collect(),
winget: winget.iter().map(|p| (*p).to_owned()).collect(),
..Default::default()
}
}
#[test]
fn each_group_goes_to_the_first_candidate_listing_packages() {
let opts = DepsOpts {
groups: vec![
group("both", &["git"], &["Git.Git"]),
group("winget-only", &[], &["Neovim.Neovim"]),
group("neither", &[], &[]),
],
manager: None,
group_filter: None,
dry_run: true,
};
let candidates: Vec<Box<dyn PackageManager>> = vec![Box::new(Scoop), Box::new(Winget)];
let (plans, unavailable) = plan_groups(&opts, &candidates);
let chosen: Vec<&str> = plans.iter().map(|p| p.manager.name()).collect();
assert_eq!(chosen, ["scoop", "winget"]);
assert_eq!(plans[1].native, [("Neovim.Neovim", "Neovim.Neovim")]);
assert_eq!(unavailable, ["neither"]);
}
#[test]
fn cargo_entries_are_split_from_the_managers_own() {
let opts = DepsOpts {
groups: vec![group(
"tools",
&[],
&["BurntSushi.ripgrep.MSVC", "cargo:hjkl"],
)],
manager: None,
group_filter: None,
dry_run: true,
};
let candidates: Vec<Box<dyn PackageManager>> = vec![Box::new(Winget)];
let (plans, _) = plan_groups(&opts, &candidates);
assert_eq!(
plans[0].native,
[("BurntSushi.ripgrep.MSVC", "BurntSushi.ripgrep.MSVC")]
);
assert_eq!(plans[0].cargo, [("cargo:hjkl", "hjkl")]);
}
}