Skip to main content

krypt_pkg/
deps.rs

1//! `krypt deps` orchestration — installs dependency groups.
2//!
3//! This module is decoupled from `krypt-core`: callers extract the relevant
4//! fields from their config and pass a [`DepGroup`] slice so that `krypt-pkg`
5//! remains free of the `krypt-core` crate dependency.
6
7use thiserror::Error;
8
9use crate::detect::{pick_by_name, pick_default};
10use crate::manager::{PackageError, PackageManager, Runner};
11
12// ─── DepGroup ─────────────────────────────────────────────────────────────────
13
14/// Caller-supplied representation of one `[[deps]]` group.
15///
16/// Mirrors the relevant fields from `krypt_core::config::DepsGroup`; the CLI
17/// layer constructs these from the parsed config so that `krypt-pkg` does not
18/// need to take a dependency on `krypt-core`.
19#[derive(Debug, Clone, Default)]
20pub struct DepGroup {
21    /// Group name (e.g. `"core"`, `"fonts"`).
22    pub group: String,
23    /// Packages for the `pacman` manager.
24    pub pacman: Vec<String>,
25    /// Packages for the `apt` manager.
26    pub apt: Vec<String>,
27    /// Packages for the `dnf` manager.
28    pub dnf: Vec<String>,
29    /// Packages for the `brew` manager.
30    pub brew: Vec<String>,
31    /// Packages for the `scoop` manager.
32    pub scoop: Vec<String>,
33    /// Packages for the `winget` manager.
34    pub winget: Vec<String>,
35}
36
37// ─── DepsError ────────────────────────────────────────────────────────────────
38
39/// Errors from [`install_deps`].
40#[derive(Debug, Error)]
41pub enum DepsError {
42    /// No package manager could be detected on this platform.
43    #[error("no package manager detected; install one or use --manager")]
44    NoManagerDetected,
45
46    /// `--manager <name>` was given but the name is unknown.
47    #[error("unknown package manager: {0}")]
48    UnknownManager(String),
49
50    /// A package installation failed.
51    #[error("install error: {0}")]
52    Install(#[from] PackageError),
53}
54
55// ─── DepsOpts ─────────────────────────────────────────────────────────────────
56
57/// Inputs for [`install_deps`].
58pub struct DepsOpts {
59    /// Dependency groups, already filtered by platform by the caller.
60    pub groups: Vec<DepGroup>,
61    /// Explicit manager override (e.g. `"apt"`). `None` = auto-detect.
62    pub manager: Option<String>,
63    /// Install only the named group. `None` = all groups.
64    pub group_filter: Option<String>,
65    /// Dry-run: skip actual installation.
66    pub dry_run: bool,
67}
68
69// ─── DepsReport ───────────────────────────────────────────────────────────────
70
71/// Summary of a [`install_deps`] run.
72pub struct DepsReport {
73    /// Name of the manager that was (or would have been) used.
74    pub manager_used: String,
75    /// Packages that were installed (or would have been in dry-run).
76    pub installed: Vec<String>,
77    /// Packages already present — skipped.
78    pub already_installed: Vec<String>,
79    /// Groups whose package list was empty for the chosen manager.
80    pub skipped_unavailable: Vec<String>,
81    /// Packages that failed to install: `(package, error_message)`.
82    pub failed: Vec<(String, String)>,
83}
84
85// ─── helpers ──────────────────────────────────────────────────────────────────
86
87/// Extract the package list for `manager_name` from a dep group.
88fn packages_for<'a>(group: &'a DepGroup, manager_name: &str) -> &'a [String] {
89    match manager_name {
90        "pacman" => &group.pacman,
91        "apt" => &group.apt,
92        "dnf" => &group.dnf,
93        "brew" => &group.brew,
94        "scoop" => &group.scoop,
95        "winget" => &group.winget,
96        _ => &[],
97    }
98}
99
100// ─── install_deps ─────────────────────────────────────────────────────────────
101
102/// Install dependency groups according to the options.
103///
104/// Groups should already be filtered by platform before calling this function.
105pub fn install_deps(opts: &DepsOpts, runner: &dyn Runner) -> Result<DepsReport, DepsError> {
106    let manager: Box<dyn PackageManager> = match &opts.manager {
107        Some(name) => pick_by_name(name).ok_or_else(|| DepsError::UnknownManager(name.clone()))?,
108        None => pick_default().ok_or(DepsError::NoManagerDetected)?,
109    };
110
111    let manager_name = manager.name().to_owned();
112    let mut report = DepsReport {
113        manager_used: manager_name.clone(),
114        installed: Vec::new(),
115        already_installed: Vec::new(),
116        skipped_unavailable: Vec::new(),
117        failed: Vec::new(),
118    };
119
120    for group in &opts.groups {
121        if opts
122            .group_filter
123            .as_deref()
124            .is_some_and(|f| f != group.group)
125        {
126            continue;
127        }
128
129        let pkgs = packages_for(group, &manager_name);
130        if pkgs.is_empty() {
131            report.skipped_unavailable.push(group.group.clone());
132            continue;
133        }
134
135        let mut to_install: Vec<String> = Vec::new();
136        if opts.dry_run {
137            // Skip is_installed check in dry-run — assume all packages need installing.
138            to_install.extend_from_slice(pkgs);
139        } else {
140            for pkg in pkgs {
141                match manager.is_installed(runner, pkg) {
142                    Ok(true) => report.already_installed.push(pkg.clone()),
143                    Ok(false) => to_install.push(pkg.clone()),
144                    Err(e) => report.failed.push((pkg.clone(), e.to_string())),
145                }
146            }
147        }
148
149        if to_install.is_empty() {
150            continue;
151        }
152
153        if opts.dry_run {
154            report.installed.extend(to_install);
155        } else {
156            match manager.install(runner, &to_install) {
157                Ok(()) => report.installed.extend(to_install),
158                Err(e) => {
159                    let msg = e.to_string();
160                    for pkg in to_install {
161                        report.failed.push((pkg, msg.clone()));
162                    }
163                }
164            }
165        }
166    }
167
168    Ok(report)
169}