use std::path::{Path, PathBuf};
use std::process::Command;
#[allow(dead_code)]
pub enum DependencyCheck {
Binary {
name: &'static str,
alternatives: &'static [&'static str],
version_cmd: Option<(&'static str, &'static [&'static str])>,
},
PythonImport {
module: &'static str,
},
Command {
program: &'static str,
args: &'static [&'static str],
},
}
pub struct Dependency {
pub name: &'static str,
pub check: DependencyCheck,
pub install: &'static str,
}
pub struct DepStatus {
pub name: &'static str,
pub ok: bool,
pub detail: String,
pub install: &'static str,
pub warning: Option<String>,
}
pub fn check_dep(dep: Dependency) -> DepStatus {
match &dep.check {
DependencyCheck::Binary {
alternatives,
version_cmd,
..
} => {
for name in *alternatives {
let found_path = which::which(name)
.map(|p| p.display().to_string())
.or_else(|_| {
for dir in extra_tool_dirs() {
let path = dir.join(name);
if path.is_file() {
return Ok(path.display().to_string());
}
}
Err(())
});
if let Ok(path) = found_path {
if let Some((probe_bin, probe_args)) = version_cmd {
let runnable = Command::new(probe_bin)
.args(*probe_args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success());
if !runnable {
return DepStatus {
name: dep.name,
ok: false,
detail: format!("{path} (found but broken — `{probe_bin}` failed to run)"),
install: dep.install,
warning: None,
};
}
}
return DepStatus {
name: dep.name,
ok: true,
detail: path,
install: dep.install,
warning: None,
};
}
}
DepStatus {
name: dep.name,
ok: false,
detail: "not found".into(),
install: dep.install,
warning: None,
}
}
DependencyCheck::PythonImport { module } => {
let ok = Command::new("python3")
.args(["-c", &format!("import {module}")])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success());
DepStatus {
name: dep.name,
ok,
detail: if ok {
format!("{module} importable")
} else {
format!("{module} not found")
},
install: dep.install,
warning: None,
}
}
DependencyCheck::Command { program, args } => {
let ok = Command::new(program)
.args(*args)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success());
DepStatus {
name: dep.name,
ok,
detail: if ok { "ok".into() } else { "failed".into() },
install: dep.install,
warning: None,
}
}
}
}
pub fn find_bin(name: &str) -> String {
if let Ok(path) = which::which(name) {
return path.display().to_string();
}
for dir in extra_tool_dirs() {
let path = dir.join(name);
if path.is_file() {
return path.display().to_string();
}
}
name.to_string()
}
pub struct ToolkitRoot {
pub path: &'static str,
pub max_depth: usize,
pub dir_filter: &'static [&'static str],
}
pub struct ToolkitAnchor {
pub bin: &'static str,
pub walk_up: usize,
}
pub struct BundledToolkit {
pub name: &'static str,
pub bin_subdir: &'static str,
pub roots: &'static [ToolkitRoot],
pub anchor: Option<ToolkitAnchor>,
}
pub fn find_bundled_tool(toolkit: &BundledToolkit, tool: &str) -> Option<PathBuf> {
for root in toolkit.roots {
if let Some(p) = probe_root(
Path::new(root.path),
root.max_depth,
root.dir_filter,
toolkit.bin_subdir,
tool,
) {
return Some(p);
}
}
if let Some(anchor) = &toolkit.anchor
&& let Some(p) = probe_anchor(anchor, toolkit.bin_subdir, tool)
{
return Some(p);
}
None
}
fn probe_root(
root: &Path,
max_depth: usize,
dir_filter: &[&str],
bin_subdir: &str,
tool: &str,
) -> Option<PathBuf> {
let candidate = root.join(bin_subdir).join(tool);
if candidate.is_file() {
return Some(candidate);
}
if max_depth == 0 {
return None;
}
let entries = std::fs::read_dir(root).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
if !dir_filter.is_empty() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !dir_filter.iter().any(|p| name.starts_with(p)) {
continue;
}
}
if let Some(found) = probe_root(&path, max_depth - 1, dir_filter, bin_subdir, tool) {
return Some(found);
}
}
None
}
pub fn find_tool_root(
anchor_bin: &str,
preferred_sibling: Option<&str>,
sibling_marker: Option<&str>,
walk_up: usize,
) -> Option<PathBuf> {
let path = which::which(anchor_bin).ok()?;
let real = std::fs::canonicalize(&path).ok()?;
if let Some(sibling) = preferred_sibling {
let mut cur: &Path = real.as_path();
for _ in 0..=walk_up {
let candidate = cur.join(sibling);
if candidate.is_dir() {
let accepted = match sibling_marker {
Some(m) => candidate.join(m).exists(),
None => true,
};
if accepted {
return Some(candidate);
}
}
match cur.parent() {
Some(p) => cur = p,
None => break,
}
}
}
real.parent().map(|p| p.to_path_buf())
}
fn probe_anchor(anchor: &ToolkitAnchor, bin_subdir: &str, tool: &str) -> Option<PathBuf> {
let path = which::which(anchor.bin).ok()?;
let real = std::fs::canonicalize(&path).ok()?;
let mut cur: &Path = real.as_path();
for _ in 0..=anchor.walk_up {
let candidate = cur.join(bin_subdir).join(tool);
if candidate.is_file() {
return Some(candidate);
}
match cur.parent() {
Some(parent) => cur = parent,
None => return None,
}
}
None
}
pub fn extra_tool_dirs() -> Vec<std::path::PathBuf> {
let mut dirs = Vec::new();
if let Ok(home) = std::env::var("HOME") {
let home = std::path::PathBuf::from(&home);
dirs.push(home.join(".dotnet/tools"));
dirs.push(home.join(".ghcup/bin"));
dirs.push(home.join(".cargo/bin"));
dirs.push(home.join(".local/bin"));
}
dirs
}
pub fn format_results(results: &[(&str, Vec<DepStatus>)]) -> String {
let mut out = String::new();
for (name, statuses) in results {
out.push_str(&format!("{name}:\n"));
for s in statuses {
let icon = if s.ok { "ok" } else { "MISSING" };
out.push_str(&format!(" {}: {} ({})\n", s.name, icon, s.detail));
if !s.ok {
out.push_str(&format!(" install: {}\n", s.install));
}
if let Some(warn) = &s.warning {
out.push_str(&format!(" WARNING: {warn}\n"));
}
}
}
out
}