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}