lean_ctx/core/
portable_binary.rs1pub fn hook_binary_override() -> Option<String> {
12 std::env::var("LEAN_CTX_HOOK_BINARY")
13 .ok()
14 .or_else(|| crate::core::config::Config::load().hook_binary.clone())
15 .map(|s| s.trim().to_string())
16 .filter(|s| !s.is_empty())
17}
18
19pub fn resolve_portable_binary() -> String {
20 let current = std::env::current_exe()
21 .ok()
22 .map(|p| p.to_string_lossy().into_owned());
23
24 let which_cmd = if cfg!(windows) { "where" } else { "which" };
25 let which_raw = std::process::Command::new(which_cmd)
26 .arg("lean-ctx")
27 .stderr(std::process::Stdio::null())
28 .output()
29 .ok()
30 .filter(|o| o.status.success())
31 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
32 .filter(|s| !s.is_empty());
33
34 choose_binary_path(current.as_deref(), which_raw.as_deref())
35}
36
37fn choose_binary_path(current_exe: Option<&str>, which_raw: Option<&str>) -> String {
59 let is_build_artifact = |p: &str| {
60 p.contains("/target/debug/")
61 || p.contains("/target/release/")
62 || p.contains("\\target\\debug\\")
63 || p.contains("\\target\\release\\")
64 };
65
66 if let Some(exe) = current_exe
68 && std::path::Path::new(exe).is_absolute()
69 && !is_build_artifact(exe)
70 {
71 return sanitize_exe_path(exe);
72 }
73
74 if let Some(raw) = which_raw {
76 let path = pick_best_binary_line(raw);
77 if std::path::Path::new(&path).is_absolute() {
78 return sanitize_exe_path(&path);
79 }
80 }
81
82 if let Some(exe) = current_exe
84 && std::path::Path::new(exe).is_absolute()
85 {
86 return sanitize_exe_path(exe);
87 }
88
89 "lean-ctx".to_string()
91}
92
93fn pick_best_binary_line(raw: &str) -> String {
97 let lines: Vec<&str> = raw
98 .lines()
99 .map(str::trim)
100 .filter(|l| !l.is_empty())
101 .collect();
102 if lines.len() <= 1 {
103 return lines.first().unwrap_or(&"lean-ctx").to_string();
104 }
105 if cfg!(windows)
106 && let Some(cmd) = lines.iter().find(|l| {
107 std::path::Path::new(*l).extension().is_some_and(|ext| {
108 ext.eq_ignore_ascii_case("cmd") || ext.eq_ignore_ascii_case("exe")
109 })
110 })
111 {
112 return cmd.to_string();
113 }
114 lines[0].to_string()
115}
116
117fn sanitize_exe_path(path: &str) -> String {
118 let cleaned = path.trim_end_matches(" (deleted)");
119 if cfg!(windows) {
120 super::pathutil::normalize_tool_path(cleaned)
121 } else {
122 cleaned.to_string()
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn single_line_returns_as_is() {
132 assert_eq!(
133 pick_best_binary_line("/usr/bin/lean-ctx"),
134 "/usr/bin/lean-ctx"
135 );
136 }
137
138 #[test]
139 fn multiline_returns_first_line() {
140 let raw = "/usr/bin/lean-ctx\n/usr/local/bin/lean-ctx";
141 let result = pick_best_binary_line(raw);
142 assert_eq!(result, "/usr/bin/lean-ctx");
143 }
144
145 #[test]
146 fn empty_returns_fallback() {
147 assert_eq!(pick_best_binary_line(""), "lean-ctx");
148 }
149
150 #[test]
151 fn sanitize_removes_deleted_suffix() {
152 assert_eq!(
153 sanitize_exe_path("/usr/bin/lean-ctx (deleted)"),
154 "/usr/bin/lean-ctx"
155 );
156 }
157
158 #[test]
159 fn whitespace_lines_are_filtered() {
160 let raw = " /usr/bin/lean-ctx \n \n /usr/local/bin/lean-ctx ";
161 assert_eq!(pick_best_binary_line(raw), "/usr/bin/lean-ctx");
162 }
163
164 #[cfg(windows)]
165 #[test]
166 fn sanitize_normalizes_msys_path_on_windows() {
167 assert_eq!(
168 sanitize_exe_path("/c/Users/ABC/.local/bin/lean-ctx"),
169 "C:/Users/ABC/.local/bin/lean-ctx"
170 );
171 }
172
173 #[cfg(windows)]
174 #[test]
175 fn sanitize_keeps_native_windows_path() {
176 assert_eq!(
177 sanitize_exe_path(r"C:\Users\ABC\lean-ctx.exe"),
178 "C:/Users/ABC/lean-ctx.exe"
179 );
180 }
181
182 #[cfg(not(windows))]
183 #[test]
184 fn sanitize_unix_path_unchanged() {
185 assert_eq!(
186 sanitize_exe_path("/usr/local/bin/lean-ctx"),
187 "/usr/local/bin/lean-ctx"
188 );
189 }
190
191 #[test]
195 fn hook_binary_override_env_is_verbatim_and_blank_is_none() {
196 let _lock = crate::core::data_dir::test_env_lock();
197 unsafe { std::env::set_var("LEAN_CTX_HOOK_BINARY", "$HOME/.local/bin/lean-ctx") };
199 assert_eq!(
200 hook_binary_override().as_deref(),
201 Some("$HOME/.local/bin/lean-ctx")
202 );
203 unsafe { std::env::set_var("LEAN_CTX_HOOK_BINARY", " ") };
205 assert_eq!(hook_binary_override(), None);
206 unsafe { std::env::remove_var("LEAN_CTX_HOOK_BINARY") };
208 }
209
210 #[test]
211 fn resolve_portable_binary_is_absolute() {
212 let resolved = resolve_portable_binary();
217 assert!(
218 std::path::Path::new(&resolved).is_absolute(),
219 "resolve_portable_binary must return an absolute path, got: {resolved}"
220 );
221 }
222
223 #[test]
224 fn nothing_resolvable_returns_bare_name() {
225 assert_eq!(choose_binary_path(None, None), "lean-ctx");
227 assert_eq!(choose_binary_path(Some("lean-ctx"), None), "lean-ctx");
229 }
230
231 #[cfg(not(windows))]
233 mod unix_paths {
234 use super::*;
235
236 #[test]
237 fn current_exe_beats_path_lookup() {
238 let chosen = choose_binary_path(
241 Some("/Users/dev/.local/bin/lean-ctx"),
242 Some("/opt/homebrew/bin/lean-ctx"),
243 );
244 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
245 }
246
247 #[test]
248 fn release_build_artifact_falls_back_to_path() {
249 let chosen = choose_binary_path(
252 Some("/work/lean-ctx/rust/target/release/lean-ctx"),
253 Some("/Users/dev/.local/bin/lean-ctx"),
254 );
255 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
256 }
257
258 #[test]
259 fn debug_build_artifact_falls_back_to_path() {
260 let chosen = choose_binary_path(
261 Some("/work/lean-ctx/rust/target/debug/deps/lean_ctx-abc123"),
262 Some("/usr/local/bin/lean-ctx"),
263 );
264 assert_eq!(chosen, "/usr/local/bin/lean-ctx");
265 }
266
267 #[test]
268 fn build_artifact_without_path_keeps_absolute_current_exe() {
269 let chosen =
272 choose_binary_path(Some("/work/lean-ctx/rust/target/release/lean-ctx"), None);
273 assert_eq!(chosen, "/work/lean-ctx/rust/target/release/lean-ctx");
274 }
275
276 #[test]
277 fn relative_current_exe_falls_back_to_path() {
278 let chosen = choose_binary_path(Some("lean-ctx"), Some("/usr/bin/lean-ctx"));
279 assert_eq!(chosen, "/usr/bin/lean-ctx");
280 }
281
282 #[test]
283 fn path_lookup_multiline_picks_first() {
284 let chosen = choose_binary_path(
285 None,
286 Some("/Users/dev/.local/bin/lean-ctx\n/opt/homebrew/bin/lean-ctx"),
287 );
288 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
289 }
290 }
291}