Skip to main content

greentic_operator/
bin_resolver.rs

1use std::path::{Path, PathBuf};
2
3pub struct ResolveCtx {
4    pub config_dir: PathBuf,
5    pub explicit_path: Option<PathBuf>,
6}
7
8pub fn resolve_binary(name: &str, ctx: &ResolveCtx) -> anyhow::Result<PathBuf> {
9    if let Some(explicit) = ctx.explicit_path.as_ref() {
10        let resolved = resolve_relative(&ctx.config_dir, explicit);
11        if resolved.exists() {
12            return Ok(resolved);
13        }
14        return Err(anyhow::anyhow!(
15            "explicit binary path not found: {}",
16            resolved.display()
17        ));
18    }
19
20    if let Some(env_path) = env_binary_override(name) {
21        if env_path.exists() {
22            return Ok(env_path);
23        }
24        return Err(anyhow::anyhow!(
25            "binary override from environment not found: {}",
26            env_path.display()
27        ));
28    }
29
30    let mut tried = Vec::new();
31
32    let local_candidates = vec![
33        ctx.config_dir.join("bin").join(binary_name(name)),
34        ctx.config_dir
35            .join("target")
36            .join("debug")
37            .join(binary_name(name)),
38        ctx.config_dir
39            .join("target")
40            .join("release")
41            .join(binary_name(name)),
42    ];
43    for candidate in local_candidates {
44        if candidate.exists() {
45            return Ok(candidate);
46        }
47        tried.push(candidate);
48    }
49
50    if let Some(path) = find_on_path(name) {
51        return Ok(path);
52    }
53
54    let mut message = format!("binary not found: {name}");
55    if !tried.is_empty() {
56        message.push_str("\nTried:");
57        for path in &tried {
58            message.push_str(&format!("\n  - {}", path.display()));
59        }
60    }
61    message.push_str(&format!(
62        "\nSuggestions:\n  - set binaries.{name} in greentic.yaml\n  - set GREENTIC_OPERATOR_BINARY_{}",
63        normalize_env_key(name)
64    ));
65    Err(anyhow::anyhow!(message))
66}
67
68fn resolve_relative(base: &Path, path: &Path) -> PathBuf {
69    if path.is_absolute() {
70        path.to_path_buf()
71    } else {
72        base.join(path)
73    }
74}
75
76fn binary_name(name: &str) -> String {
77    if cfg!(windows) {
78        if name.ends_with(".exe") {
79            name.to_string()
80        } else {
81            format!("{name}.exe")
82        }
83    } else {
84        name.to_string()
85    }
86}
87
88fn find_on_path(binary: &str) -> Option<PathBuf> {
89    let path_var = std::env::var_os("PATH")?;
90    for dir in std::env::split_paths(&path_var) {
91        let candidate = dir.join(binary_name(binary));
92        if candidate.is_file() {
93            return Some(candidate);
94        }
95    }
96    None
97}
98
99fn env_binary_override(name: &str) -> Option<PathBuf> {
100    let key = format!("GREENTIC_OPERATOR_BINARY_{}", normalize_env_key(name));
101    std::env::var_os(key).map(PathBuf::from)
102}
103
104fn normalize_env_key(name: &str) -> String {
105    name.chars()
106        .map(|ch| {
107            if ch.is_ascii_alphanumeric() {
108                ch.to_ascii_uppercase()
109            } else {
110                '_'
111            }
112        })
113        .collect::<String>()
114}