1use std::path::{Path, PathBuf};
19
20use crate::probe::{Toolbox, Version};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Origin {
25 Override,
27 Path,
29 Sibling,
31}
32
33impl Origin {
34 pub fn token(self) -> &'static str {
35 match self {
36 Origin::Override => "override",
37 Origin::Path => "path",
38 Origin::Sibling => "sibling",
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum Skew {
48 Match,
50 Minor,
52 Major,
54}
55
56impl Skew {
57 pub fn classify(driver: Version, compiler: Version) -> Skew {
59 if driver.major != compiler.major {
60 Skew::Major
61 } else if driver.minor != compiler.minor {
62 Skew::Minor
63 } else {
64 Skew::Match
65 }
66 }
67
68 pub fn token(self) -> &'static str {
69 match self {
70 Skew::Match => "match",
71 Skew::Minor => "minor",
72 Skew::Major => "major",
73 }
74 }
75}
76
77#[derive(Debug, Clone)]
79pub struct Compiler {
80 pub path: Option<PathBuf>,
83 pub origin: Option<Origin>,
84 pub version: Option<Version>,
85 pub skew: Option<Skew>,
87}
88
89impl Compiler {
90 pub fn is_resolved(&self) -> bool {
91 self.path.is_some()
92 }
93
94 pub fn has_major_skew(&self) -> bool {
96 self.skew == Some(Skew::Major)
97 }
98}
99
100pub fn resolve(
104 tb: &dyn Toolbox,
105 override_path: Option<&Path>,
106 bynk_bin_dir: Option<&Path>,
107 driver: Version,
108) -> Compiler {
109 let (path, origin) = locate(tb, override_path, bynk_bin_dir);
110 let version = path.as_deref().and_then(|p| tb.version(p));
111 let skew = version.map(|v| Skew::classify(driver, v));
112 Compiler {
113 path,
114 origin,
115 version,
116 skew,
117 }
118}
119
120fn locate(
121 tb: &dyn Toolbox,
122 override_path: Option<&Path>,
123 bynk_bin_dir: Option<&Path>,
124) -> (Option<PathBuf>, Option<Origin>) {
125 if let Some(ovr) = override_path.filter(|p| !p.as_os_str().is_empty()) {
128 let dir = ovr
135 .parent()
136 .filter(|d| !d.as_os_str().is_empty())
137 .unwrap_or(Path::new("."));
138 let name = ovr.file_name().and_then(|n| n.to_str()).unwrap_or_default();
139 if let Some(p) = tb.in_dir(dir, name) {
140 return (Some(p), Some(Origin::Override));
141 }
142 return (None, Some(Origin::Override));
147 }
148 if let Some(p) = tb.on_path("bynkc") {
149 return (Some(p), Some(Origin::Path));
150 }
151 if let Some(dir) = bynk_bin_dir
152 && let Some(p) = tb.in_dir(dir, "bynkc")
153 {
154 return (Some(p), Some(Origin::Sibling));
155 }
156 (None, None)
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 struct FakeToolbox {
166 files: Vec<(PathBuf, String)>,
167 on_path: Vec<String>,
168 }
169
170 impl Toolbox for FakeToolbox {
171 fn on_path(&self, tool: &str) -> Option<PathBuf> {
172 self.on_path
173 .iter()
174 .any(|t| t == tool)
175 .then(|| PathBuf::from("/usr/bin").join(tool))
176 }
177 fn in_dir(&self, dir: &Path, tool: &str) -> Option<PathBuf> {
178 self.files
179 .iter()
180 .any(|(d, n)| d == dir && n == tool)
181 .then(|| dir.join(tool))
182 }
183 fn version(&self, _path: &Path) -> Option<Version> {
184 None
185 }
186 fn npx_available(&self) -> bool {
187 false
188 }
189 }
190
191 #[test]
192 fn missing_override_resolves_to_none() {
193 let tb = FakeToolbox {
196 files: vec![],
197 on_path: vec!["bynkc".into()],
198 };
199 let (path, origin) = locate(&tb, Some(Path::new("/opt/missing/bynkc")), None);
200 assert_eq!(path, None);
201 assert_eq!(origin, Some(Origin::Override));
202 }
203
204 #[test]
205 fn empty_override_is_unset() {
206 let tb = FakeToolbox {
208 files: vec![(PathBuf::from("."), "bynkc".into())],
209 on_path: vec!["bynkc".into()],
210 };
211 let (path, origin) = locate(&tb, Some(Path::new("")), None);
212 assert_eq!(origin, Some(Origin::Path));
213 assert_eq!(path, Some(PathBuf::from("/usr/bin/bynkc")));
214 }
215
216 #[test]
217 fn override_never_resolves_by_stem() {
218 let tb = FakeToolbox {
221 files: vec![(PathBuf::from("/dir"), "bynkc".into())],
222 on_path: vec![],
223 };
224 let (path, origin) = locate(&tb, Some(Path::new("/dir/bynkc.backup")), None);
225 assert_eq!(path, None, "the named backup binary does not exist");
226 assert_eq!(origin, Some(Origin::Override));
227
228 let tb = FakeToolbox {
230 files: vec![(PathBuf::from("/dir"), "bynkc.backup".into())],
231 on_path: vec![],
232 };
233 let (path, _) = locate(&tb, Some(Path::new("/dir/bynkc.backup")), None);
234 assert_eq!(path, Some(PathBuf::from("/dir/bynkc.backup")));
235 }
236
237 #[test]
238 fn skew_classification() {
239 let v = |a, b, c| Version {
240 major: a,
241 minor: b,
242 patch: c,
243 };
244 assert_eq!(Skew::classify(v(0, 46, 0), v(0, 46, 0)), Skew::Match);
245 assert_eq!(Skew::classify(v(0, 46, 0), v(0, 46, 3)), Skew::Match);
247 assert_eq!(Skew::classify(v(0, 46, 0), v(0, 44, 0)), Skew::Minor);
248 assert_eq!(Skew::classify(v(1, 0, 0), v(0, 46, 0)), Skew::Major);
249 }
250}