lean_ctx/core/
portable_binary.rs1pub fn resolve_portable_binary() -> String {
2 let current = std::env::current_exe()
3 .ok()
4 .map(|p| p.to_string_lossy().into_owned());
5
6 let which_cmd = if cfg!(windows) { "where" } else { "which" };
7 let which_raw = std::process::Command::new(which_cmd)
8 .arg("lean-ctx")
9 .stderr(std::process::Stdio::null())
10 .output()
11 .ok()
12 .filter(|o| o.status.success())
13 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
14 .filter(|s| !s.is_empty());
15
16 choose_binary_path(current.as_deref(), which_raw.as_deref())
17}
18
19fn choose_binary_path(current_exe: Option<&str>, which_raw: Option<&str>) -> String {
41 let is_build_artifact = |p: &str| {
42 p.contains("/target/debug/")
43 || p.contains("/target/release/")
44 || p.contains("\\target\\debug\\")
45 || p.contains("\\target\\release\\")
46 };
47
48 if let Some(exe) = current_exe
50 && std::path::Path::new(exe).is_absolute()
51 && !is_build_artifact(exe)
52 {
53 return sanitize_exe_path(exe);
54 }
55
56 if let Some(raw) = which_raw {
58 let path = pick_best_binary_line(raw);
59 if std::path::Path::new(&path).is_absolute() {
60 return sanitize_exe_path(&path);
61 }
62 }
63
64 if let Some(exe) = current_exe
66 && std::path::Path::new(exe).is_absolute()
67 {
68 return sanitize_exe_path(exe);
69 }
70
71 "lean-ctx".to_string()
73}
74
75fn pick_best_binary_line(raw: &str) -> String {
79 let lines: Vec<&str> = raw
80 .lines()
81 .map(str::trim)
82 .filter(|l| !l.is_empty())
83 .collect();
84 if lines.len() <= 1 {
85 return lines.first().unwrap_or(&"lean-ctx").to_string();
86 }
87 if cfg!(windows)
88 && let Some(cmd) = lines.iter().find(|l| {
89 std::path::Path::new(*l).extension().is_some_and(|ext| {
90 ext.eq_ignore_ascii_case("cmd") || ext.eq_ignore_ascii_case("exe")
91 })
92 })
93 {
94 return cmd.to_string();
95 }
96 lines[0].to_string()
97}
98
99fn sanitize_exe_path(path: &str) -> String {
100 let cleaned = path.trim_end_matches(" (deleted)");
101 if cfg!(windows) {
102 super::pathutil::normalize_tool_path(cleaned)
103 } else {
104 cleaned.to_string()
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn single_line_returns_as_is() {
114 assert_eq!(
115 pick_best_binary_line("/usr/bin/lean-ctx"),
116 "/usr/bin/lean-ctx"
117 );
118 }
119
120 #[test]
121 fn multiline_returns_first_line() {
122 let raw = "/usr/bin/lean-ctx\n/usr/local/bin/lean-ctx";
123 let result = pick_best_binary_line(raw);
124 assert_eq!(result, "/usr/bin/lean-ctx");
125 }
126
127 #[test]
128 fn empty_returns_fallback() {
129 assert_eq!(pick_best_binary_line(""), "lean-ctx");
130 }
131
132 #[test]
133 fn sanitize_removes_deleted_suffix() {
134 assert_eq!(
135 sanitize_exe_path("/usr/bin/lean-ctx (deleted)"),
136 "/usr/bin/lean-ctx"
137 );
138 }
139
140 #[test]
141 fn whitespace_lines_are_filtered() {
142 let raw = " /usr/bin/lean-ctx \n \n /usr/local/bin/lean-ctx ";
143 assert_eq!(pick_best_binary_line(raw), "/usr/bin/lean-ctx");
144 }
145
146 #[cfg(windows)]
147 #[test]
148 fn sanitize_normalizes_msys_path_on_windows() {
149 assert_eq!(
150 sanitize_exe_path("/c/Users/ABC/.local/bin/lean-ctx"),
151 "C:/Users/ABC/.local/bin/lean-ctx"
152 );
153 }
154
155 #[cfg(windows)]
156 #[test]
157 fn sanitize_keeps_native_windows_path() {
158 assert_eq!(
159 sanitize_exe_path(r"C:\Users\ABC\lean-ctx.exe"),
160 "C:/Users/ABC/lean-ctx.exe"
161 );
162 }
163
164 #[cfg(not(windows))]
165 #[test]
166 fn sanitize_unix_path_unchanged() {
167 assert_eq!(
168 sanitize_exe_path("/usr/local/bin/lean-ctx"),
169 "/usr/local/bin/lean-ctx"
170 );
171 }
172
173 #[test]
174 fn resolve_portable_binary_is_absolute() {
175 let resolved = resolve_portable_binary();
180 assert!(
181 std::path::Path::new(&resolved).is_absolute(),
182 "resolve_portable_binary must return an absolute path, got: {resolved}"
183 );
184 }
185
186 #[test]
187 fn nothing_resolvable_returns_bare_name() {
188 assert_eq!(choose_binary_path(None, None), "lean-ctx");
190 assert_eq!(choose_binary_path(Some("lean-ctx"), None), "lean-ctx");
192 }
193
194 #[cfg(not(windows))]
196 mod unix_paths {
197 use super::*;
198
199 #[test]
200 fn current_exe_beats_path_lookup() {
201 let chosen = choose_binary_path(
204 Some("/Users/dev/.local/bin/lean-ctx"),
205 Some("/opt/homebrew/bin/lean-ctx"),
206 );
207 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
208 }
209
210 #[test]
211 fn release_build_artifact_falls_back_to_path() {
212 let chosen = choose_binary_path(
215 Some("/work/lean-ctx/rust/target/release/lean-ctx"),
216 Some("/Users/dev/.local/bin/lean-ctx"),
217 );
218 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
219 }
220
221 #[test]
222 fn debug_build_artifact_falls_back_to_path() {
223 let chosen = choose_binary_path(
224 Some("/work/lean-ctx/rust/target/debug/deps/lean_ctx-abc123"),
225 Some("/usr/local/bin/lean-ctx"),
226 );
227 assert_eq!(chosen, "/usr/local/bin/lean-ctx");
228 }
229
230 #[test]
231 fn build_artifact_without_path_keeps_absolute_current_exe() {
232 let chosen =
235 choose_binary_path(Some("/work/lean-ctx/rust/target/release/lean-ctx"), None);
236 assert_eq!(chosen, "/work/lean-ctx/rust/target/release/lean-ctx");
237 }
238
239 #[test]
240 fn relative_current_exe_falls_back_to_path() {
241 let chosen = choose_binary_path(Some("lean-ctx"), Some("/usr/bin/lean-ctx"));
242 assert_eq!(chosen, "/usr/bin/lean-ctx");
243 }
244
245 #[test]
246 fn path_lookup_multiline_picks_first() {
247 let chosen = choose_binary_path(
248 None,
249 Some("/Users/dev/.local/bin/lean-ctx\n/opt/homebrew/bin/lean-ctx"),
250 );
251 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
252 }
253 }
254}