lean_toolchain/
discover.rs1use std::env;
15use std::fs;
16use std::path::{Path, PathBuf};
17use std::process::Command;
18
19use crate::diagnostics::LinkDiagnostics;
20use crate::fingerprint::ToolchainFingerprint;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum DiscoverySource {
25 ExplicitSysroot,
27 LeanSysrootEnv,
29 ElanHome,
31 Path,
33 LakeFixtureEnv,
35}
36
37#[allow(clippy::struct_excessive_bools)]
44#[derive(Clone, Debug)]
45pub struct DiscoverOptions {
46 pub explicit_sysroot: Option<PathBuf>,
51 pub allow_lean_sysroot_env: bool,
53 pub allow_path_lookup: bool,
55 pub allow_elan: bool,
57 pub allow_lake_env: bool,
59 pub toolchain_file: Option<PathBuf>,
61}
62
63impl Default for DiscoverOptions {
64 fn default() -> Self {
65 Self {
66 explicit_sysroot: None,
67 allow_lean_sysroot_env: true,
68 allow_path_lookup: true,
69 allow_elan: true,
70 allow_lake_env: true,
71 toolchain_file: None,
72 }
73 }
74}
75
76#[derive(Clone, Debug)]
78pub struct ToolchainInfo {
79 pub prefix: PathBuf,
81 pub lean_binary: Option<PathBuf>,
83 pub header_path: PathBuf,
85 pub lib_dir: PathBuf,
87 pub version: String,
91 pub fingerprint: ToolchainFingerprint,
93 pub source: DiscoverySource,
95}
96
97pub fn discover_toolchain(opts: &DiscoverOptions) -> Result<ToolchainInfo, LinkDiagnostics> {
105 let mut tried: Vec<String> = Vec::new();
106
107 if let Some(sysroot) = opts.explicit_sysroot.as_ref() {
108 if has_header(sysroot) {
109 return Ok(build_info(sysroot.clone(), DiscoverySource::ExplicitSysroot, opts));
110 }
111 tried.push(format!(
112 "explicit_sysroot={} (no include/lean/lean.h)",
113 sysroot.display()
114 ));
115 } else {
116 tried.push("explicit_sysroot unset".into());
117 }
118
119 if opts.allow_lean_sysroot_env {
120 match env::var_os("LEAN_SYSROOT") {
121 Some(value) => {
122 let path = PathBuf::from(&value);
123 if has_header(&path) {
124 return Ok(build_info(path, DiscoverySource::LeanSysrootEnv, opts));
125 }
126 tried.push(format!("LEAN_SYSROOT={} (no include/lean/lean.h)", path.display()));
127 }
128 None => tried.push("LEAN_SYSROOT unset".into()),
129 }
130 } else {
131 tried.push("LEAN_SYSROOT probe disabled".into());
132 }
133
134 if opts.allow_elan {
135 match elan_prefix() {
136 Ok(Some(path)) => {
137 if has_header(&path) {
138 return Ok(build_info(path, DiscoverySource::ElanHome, opts));
139 }
140 tried.push(format!("elan toolchain {} (no include/lean/lean.h)", path.display()));
141 }
142 Ok(None) => tried.push("ELAN_HOME unset or elan unavailable".into()),
143 Err(reason) => tried.push(reason),
144 }
145 } else {
146 tried.push("elan probe disabled".into());
147 }
148
149 if opts.allow_path_lookup {
150 match lean_print_prefix() {
151 Ok(Some(path)) => {
152 if has_header(&path) {
153 return Ok(build_info(path, DiscoverySource::Path, opts));
154 }
155 tried.push(format!(
156 "`lean --print-prefix` = {} (no include/lean/lean.h)",
157 path.display()
158 ));
159 }
160 Ok(None) => tried.push("`lean --print-prefix` returned nothing".into()),
161 Err(reason) => tried.push(reason),
162 }
163 } else {
164 tried.push("PATH lookup disabled".into());
165 }
166
167 if opts.allow_lake_env {
168 match lake_fixture_prefix() {
169 Ok(Some(path)) => {
170 if has_header(&path) {
171 return Ok(build_info(path, DiscoverySource::LakeFixtureEnv, opts));
172 }
173 tried.push(format!(
174 "lake fixture prefix {} (no include/lean/lean.h)",
175 path.display()
176 ));
177 }
178 Ok(None) => tried.push("lake fixture probe: no workspace `fixtures/lean` found".into()),
179 Err(reason) => tried.push(reason),
180 }
181 } else {
182 tried.push("lake env probe disabled".into());
183 }
184
185 Err(LinkDiagnostics::MissingLean { tried })
186}
187
188fn has_header(prefix: &Path) -> bool {
189 prefix.join("include").join("lean").join("lean.h").is_file()
190}
191
192fn build_info(prefix: PathBuf, source: DiscoverySource, opts: &DiscoverOptions) -> ToolchainInfo {
193 let header_path = prefix.join("include").join("lean").join("lean.h");
194 let lib_dir = prefix.join("lib");
195 let lean_binary = {
196 let candidate = prefix.join("bin").join("lean");
197 if candidate.is_file() { Some(candidate) } else { None }
198 };
199 let version = opts
200 .toolchain_file
201 .as_deref()
202 .and_then(parse_toolchain_file)
203 .or_else(|| parse_version_header(&prefix))
204 .unwrap_or_else(|| crate::LEAN_VERSION.to_string());
205 ToolchainInfo {
206 prefix,
207 lean_binary,
208 header_path,
209 lib_dir,
210 version,
211 fingerprint: ToolchainFingerprint::current(),
212 source,
213 }
214}
215
216fn parse_toolchain_file(path: &Path) -> Option<String> {
217 let text = fs::read_to_string(path).ok()?;
218 let line = text.lines().next()?.trim();
221 let (_channel, tag) = line.split_once(':')?;
222 Some(tag.trim_start_matches('v').to_string())
223}
224
225fn parse_version_header(prefix: &Path) -> Option<String> {
226 let version_h = prefix.join("include").join("lean").join("version.h");
227 let text = fs::read_to_string(&version_h).ok()?;
228 for line in text.lines() {
229 if let Some(rest) = line.trim().strip_prefix("#define LEAN_VERSION_STRING") {
230 let trimmed = rest.trim().trim_matches('"');
231 if !trimmed.is_empty() {
232 return Some(trimmed.to_string());
233 }
234 }
235 }
236 None
237}
238
239fn elan_prefix() -> Result<Option<PathBuf>, String> {
240 let Some(elan_home) = env::var_os("ELAN_HOME") else {
241 return Ok(None);
242 };
243 let output = Command::new("elan")
244 .args(["show", "active-toolchain"])
245 .output()
246 .map_err(|err| format!("`elan show active-toolchain` failed: {err}"))?;
247 if !output.status.success() {
248 return Err(format!("`elan show active-toolchain` exited {}", output.status));
249 }
250 let toolchain = String::from_utf8_lossy(&output.stdout)
251 .lines()
252 .next()
253 .unwrap_or("")
254 .split_whitespace()
255 .next()
256 .unwrap_or("")
257 .to_string();
258 if toolchain.is_empty() {
259 return Ok(None);
260 }
261 Ok(Some(PathBuf::from(elan_home).join("toolchains").join(toolchain)))
262}
263
264fn lean_print_prefix() -> Result<Option<PathBuf>, String> {
265 let output = Command::new("lean")
266 .arg("--print-prefix")
267 .output()
268 .map_err(|err| format!("`lean --print-prefix` failed: {err}"))?;
269 if !output.status.success() {
270 return Err(format!("`lean --print-prefix` exited {}", output.status));
271 }
272 let prefix = String::from_utf8_lossy(&output.stdout).trim().to_string();
273 if prefix.is_empty() {
274 Ok(None)
275 } else {
276 Ok(Some(PathBuf::from(prefix)))
277 }
278}
279
280fn lake_fixture_prefix() -> Result<Option<PathBuf>, String> {
281 let Some(fixture_dir) = find_workspace_fixture() else {
282 return Ok(None);
283 };
284 let output = Command::new("lake")
285 .args(["env", "printenv", "LEAN_SYSROOT"])
286 .current_dir(&fixture_dir)
287 .output()
288 .map_err(|err| format!("`lake env printenv LEAN_SYSROOT` failed: {err}"))?;
289 if !output.status.success() {
290 return Err(format!("`lake env printenv LEAN_SYSROOT` exited {}", output.status));
291 }
292 let prefix = String::from_utf8_lossy(&output.stdout).trim().to_string();
293 if prefix.is_empty() {
294 Ok(None)
295 } else {
296 Ok(Some(PathBuf::from(prefix)))
297 }
298}
299
300fn find_workspace_fixture() -> Option<PathBuf> {
301 let start = env::var_os("CARGO_MANIFEST_DIR")
304 .map(PathBuf::from)
305 .or_else(|| env::current_dir().ok())?;
306 let mut cursor: &Path = start.as_path();
307 loop {
308 let candidate = cursor.join("fixtures").join("lean");
309 if candidate.join("lakefile.lean").is_file() {
310 return Some(candidate);
311 }
312 cursor = cursor.parent()?;
313 }
314}