Skip to main content

dbg_cli/
deps.rs

1//! Shared dependency-checking infrastructure.
2//!
3//! Used by both `dbg` and `gdbg` to verify tool availability.
4
5use std::process::Command;
6
7/// How to verify a dependency is installed.
8#[allow(dead_code)]
9pub enum DependencyCheck {
10    /// Check that a binary exists on PATH (optionally with minimum version).
11    Binary {
12        name: &'static str,
13        /// Alternative names to try (e.g., "lldb-20", "lldb-18", "lldb").
14        alternatives: &'static [&'static str],
15        /// Command + args to get version string, e.g., ("lldb-20", &["--version"]).
16        /// If None, just checks existence.
17        version_cmd: Option<(&'static str, &'static [&'static str])>,
18    },
19    /// Check that a Python module can be imported.
20    PythonImport {
21        module: &'static str,
22    },
23    /// Run an arbitrary command; exit code 0 means installed.
24    Command {
25        program: &'static str,
26        args: &'static [&'static str],
27    },
28}
29
30/// A single dependency with its check and install instructions.
31pub struct Dependency {
32    pub name: &'static str,
33    pub check: DependencyCheck,
34    pub install: &'static str,
35}
36
37/// Result of checking a single dependency.
38pub struct DepStatus {
39    pub name: &'static str,
40    pub ok: bool,
41    /// The resolved path or version if found.
42    pub detail: String,
43    /// Install instructions if not found.
44    pub install: &'static str,
45    /// Optional warning (tool found but degraded).
46    pub warning: Option<String>,
47}
48
49/// Check a single dependency.
50pub fn check_dep(dep: Dependency) -> DepStatus {
51    match &dep.check {
52        DependencyCheck::Binary {
53            alternatives,
54            ..
55        } => {
56            for name in *alternatives {
57                if let Ok(path) = which::which(name) {
58                    return DepStatus {
59                        name: dep.name,
60                        ok: true,
61                        detail: path.display().to_string(),
62                        install: dep.install,
63                        warning: None,
64                    };
65                }
66                for dir in extra_tool_dirs() {
67                    let path = dir.join(name);
68                    if path.is_file() {
69                        return DepStatus {
70                            name: dep.name,
71                            ok: true,
72                            detail: path.display().to_string(),
73                            install: dep.install,
74                            warning: None,
75                        };
76                    }
77                }
78            }
79            DepStatus {
80                name: dep.name,
81                ok: false,
82                detail: "not found".into(),
83                install: dep.install,
84                warning: None,
85            }
86        }
87        DependencyCheck::PythonImport { module } => {
88            let ok = Command::new("python3")
89                .args(["-c", &format!("import {module}")])
90                .stdout(std::process::Stdio::null())
91                .stderr(std::process::Stdio::null())
92                .status()
93                .is_ok_and(|s| s.success());
94            DepStatus {
95                name: dep.name,
96                ok,
97                detail: if ok {
98                    format!("{module} importable")
99                } else {
100                    format!("{module} not found")
101                },
102                install: dep.install,
103                warning: None,
104            }
105        }
106        DependencyCheck::Command { program, args } => {
107            let ok = Command::new(program)
108                .args(*args)
109                .stdout(std::process::Stdio::null())
110                .stderr(std::process::Stdio::null())
111                .status()
112                .is_ok_and(|s| s.success());
113            DepStatus {
114                name: dep.name,
115                ok,
116                detail: if ok { "ok".into() } else { "failed".into() },
117                install: dep.install,
118                warning: None,
119            }
120        }
121    }
122}
123
124/// Resolve a binary name to its full path, checking PATH and extra tool dirs.
125/// Returns the full path if found, or the original name as fallback.
126pub fn find_bin(name: &str) -> String {
127    if let Ok(path) = which::which(name) {
128        return path.display().to_string();
129    }
130    for dir in extra_tool_dirs() {
131        let path = dir.join(name);
132        if path.is_file() {
133            return path.display().to_string();
134        }
135    }
136    name.to_string()
137}
138
139/// Extra directories to search for tool binaries not on PATH.
140pub fn extra_tool_dirs() -> Vec<std::path::PathBuf> {
141    let mut dirs = Vec::new();
142    if let Ok(home) = std::env::var("HOME") {
143        let home = std::path::PathBuf::from(&home);
144        dirs.push(home.join(".dotnet/tools"));
145        dirs.push(home.join(".ghcup/bin"));
146        dirs.push(home.join(".cargo/bin"));
147        dirs.push(home.join(".local/bin"));
148    }
149    dirs
150}
151
152/// Format check results for display.
153pub fn format_results(results: &[(&str, Vec<DepStatus>)]) -> String {
154    let mut out = String::new();
155    for (name, statuses) in results {
156        out.push_str(&format!("{name}:\n"));
157        for s in statuses {
158            let icon = if s.ok { "ok" } else { "MISSING" };
159            out.push_str(&format!("  {}: {} ({})\n", s.name, icon, s.detail));
160            if !s.ok {
161                out.push_str(&format!("    install: {}\n", s.install));
162            }
163            if let Some(warn) = &s.warning {
164                out.push_str(&format!("    WARNING: {warn}\n"));
165            }
166        }
167    }
168    out
169}