greentic_operator/
bin_resolver.rs1use 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}