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 if std::path::Path::new(exe).is_absolute() && !is_build_artifact(exe) {
51 return sanitize_exe_path(exe);
52 }
53 }
54
55 if let Some(raw) = which_raw {
57 let path = pick_best_binary_line(raw);
58 if std::path::Path::new(&path).is_absolute() {
59 return sanitize_exe_path(&path);
60 }
61 }
62
63 if let Some(exe) = current_exe {
65 if std::path::Path::new(exe).is_absolute() {
66 return sanitize_exe_path(exe);
67 }
68 }
69
70 "lean-ctx".to_string()
72}
73
74fn pick_best_binary_line(raw: &str) -> String {
78 let lines: Vec<&str> = raw
79 .lines()
80 .map(str::trim)
81 .filter(|l| !l.is_empty())
82 .collect();
83 if lines.len() <= 1 {
84 return lines.first().unwrap_or(&"lean-ctx").to_string();
85 }
86 if cfg!(windows) {
87 if let Some(cmd) = lines.iter().find(|l| {
88 std::path::Path::new(*l).extension().is_some_and(|ext| {
89 ext.eq_ignore_ascii_case("cmd") || ext.eq_ignore_ascii_case("exe")
90 })
91 }) {
92 return cmd.to_string();
93 }
94 }
95 lines[0].to_string()
96}
97
98fn sanitize_exe_path(path: &str) -> String {
99 let cleaned = path.trim_end_matches(" (deleted)");
100 if cfg!(windows) {
101 super::pathutil::normalize_tool_path(cleaned)
102 } else {
103 cleaned.to_string()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn single_line_returns_as_is() {
113 assert_eq!(
114 pick_best_binary_line("/usr/bin/lean-ctx"),
115 "/usr/bin/lean-ctx"
116 );
117 }
118
119 #[test]
120 fn multiline_returns_first_line() {
121 let raw = "/usr/bin/lean-ctx\n/usr/local/bin/lean-ctx";
122 let result = pick_best_binary_line(raw);
123 assert_eq!(result, "/usr/bin/lean-ctx");
124 }
125
126 #[test]
127 fn empty_returns_fallback() {
128 assert_eq!(pick_best_binary_line(""), "lean-ctx");
129 }
130
131 #[test]
132 fn sanitize_removes_deleted_suffix() {
133 assert_eq!(
134 sanitize_exe_path("/usr/bin/lean-ctx (deleted)"),
135 "/usr/bin/lean-ctx"
136 );
137 }
138
139 #[test]
140 fn whitespace_lines_are_filtered() {
141 let raw = " /usr/bin/lean-ctx \n \n /usr/local/bin/lean-ctx ";
142 assert_eq!(pick_best_binary_line(raw), "/usr/bin/lean-ctx");
143 }
144
145 #[cfg(windows)]
146 #[test]
147 fn sanitize_normalizes_msys_path_on_windows() {
148 assert_eq!(
149 sanitize_exe_path("/c/Users/ABC/.local/bin/lean-ctx"),
150 "C:/Users/ABC/.local/bin/lean-ctx"
151 );
152 }
153
154 #[cfg(windows)]
155 #[test]
156 fn sanitize_keeps_native_windows_path() {
157 assert_eq!(
158 sanitize_exe_path(r"C:\Users\ABC\lean-ctx.exe"),
159 "C:/Users/ABC/lean-ctx.exe"
160 );
161 }
162
163 #[cfg(not(windows))]
164 #[test]
165 fn sanitize_unix_path_unchanged() {
166 assert_eq!(
167 sanitize_exe_path("/usr/local/bin/lean-ctx"),
168 "/usr/local/bin/lean-ctx"
169 );
170 }
171
172 #[test]
173 fn resolve_portable_binary_is_absolute() {
174 let resolved = resolve_portable_binary();
179 assert!(
180 std::path::Path::new(&resolved).is_absolute(),
181 "resolve_portable_binary must return an absolute path, got: {resolved}"
182 );
183 }
184
185 #[test]
186 fn nothing_resolvable_returns_bare_name() {
187 assert_eq!(choose_binary_path(None, None), "lean-ctx");
189 assert_eq!(choose_binary_path(Some("lean-ctx"), None), "lean-ctx");
191 }
192
193 #[cfg(not(windows))]
195 mod unix_paths {
196 use super::*;
197
198 #[test]
199 fn current_exe_beats_path_lookup() {
200 let chosen = choose_binary_path(
203 Some("/Users/dev/.local/bin/lean-ctx"),
204 Some("/opt/homebrew/bin/lean-ctx"),
205 );
206 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
207 }
208
209 #[test]
210 fn release_build_artifact_falls_back_to_path() {
211 let chosen = choose_binary_path(
214 Some("/work/lean-ctx/rust/target/release/lean-ctx"),
215 Some("/Users/dev/.local/bin/lean-ctx"),
216 );
217 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
218 }
219
220 #[test]
221 fn debug_build_artifact_falls_back_to_path() {
222 let chosen = choose_binary_path(
223 Some("/work/lean-ctx/rust/target/debug/deps/lean_ctx-abc123"),
224 Some("/usr/local/bin/lean-ctx"),
225 );
226 assert_eq!(chosen, "/usr/local/bin/lean-ctx");
227 }
228
229 #[test]
230 fn build_artifact_without_path_keeps_absolute_current_exe() {
231 let chosen =
234 choose_binary_path(Some("/work/lean-ctx/rust/target/release/lean-ctx"), None);
235 assert_eq!(chosen, "/work/lean-ctx/rust/target/release/lean-ctx");
236 }
237
238 #[test]
239 fn relative_current_exe_falls_back_to_path() {
240 let chosen = choose_binary_path(Some("lean-ctx"), Some("/usr/bin/lean-ctx"));
241 assert_eq!(chosen, "/usr/bin/lean-ctx");
242 }
243
244 #[test]
245 fn path_lookup_multiline_picks_first() {
246 let chosen = choose_binary_path(
247 None,
248 Some("/Users/dev/.local/bin/lean-ctx\n/opt/homebrew/bin/lean-ctx"),
249 );
250 assert_eq!(chosen, "/Users/dev/.local/bin/lean-ctx");
251 }
252 }
253}