From 0898a209c93a2d067840b6fff05e21a82ead01a6 Mon Sep 17 00:00:00 2001 From: Jason Reed Date: Wed, 17 Sep 2025 12:21:54 -0400 Subject: [PATCH] feat: show target of links in in "elan show" When doing `elan show` after ``` elan toolchain link lean4-pgit /home/user/build/release/stage1 ``` the output now shows ``` installed toolchains -------------------- lean4 (linked to local path /home/user/build/release/stage1) leanprover/lean4:v4.22.0 leanprover/lean4:v4.23.0 (resolved from default 'stable') leanprover/lean4:v4.24.0-rc1 active toolchain ---------------- leanprover/lean4:v4.23.0 (resolved from default 'stable') Lean (version 4.23.0, x86_64-unknown-linux-gnu, commit 50aaf682e9b74ab92880292a25c68baa1cc81c87, Release) ``` with the novelty being the "(linked to local path /home/user/build/release/stage1)" message. --- src/elan-cli/elan_mode.rs | 25 +++++++++++++++++++++---- src/elan-utils/src/errors.rs | 6 ++++++ src/elan-utils/src/utils.rs | 28 ++++++++++++++++++++++++---- src/elan/toolchain.rs | 10 +++++++--- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/elan-cli/elan_mode.rs b/src/elan-cli/elan_mode.rs index e50ed8a..96d8aef 100644 --- a/src/elan-cli/elan_mode.rs +++ b/src/elan-cli/elan_mode.rs @@ -332,6 +332,13 @@ pub fn list_toolchains(cfg: &Cfg) -> Result<()> { Ok(()) } +fn get_toolchain_local_target(cfg: &Cfg, t: &ToolchainDesc) -> Result> { + let ToolchainDesc::Local { .. } = &t else { + return Ok(None); + }; + Ok(cfg.get_toolchain(&t, false)?.symlink_target()?) +} + fn show(cfg: &Cfg) -> Result<()> { let cwd = &(utils::current_dir()?); let installed_toolchains = cfg.list_toolchains()?; @@ -357,10 +364,20 @@ fn show(cfg: &Cfg) -> Result<()> { print_header("installed toolchains") } for t in installed_toolchains { - println!( - "{}", - mk_toolchain_label(&t, &default_tc, &resolved_default_tc) - ); + // If t is a local toolchain, look up the symlink and print + // where it points. + if let Some(path) = get_toolchain_local_target(&cfg, &t)? { + println!( + "{} (linked to local path {})", + &t, + path.display().to_string() + ); + } else { + println!( + "{}", + mk_toolchain_label(&t, &default_tc, &resolved_default_tc) + ); + } } if show_headers { println!() diff --git a/src/elan-utils/src/errors.rs b/src/elan-utils/src/errors.rs index 1a6c4a5..a8b5dfa 100644 --- a/src/elan-utils/src/errors.rs +++ b/src/elan-utils/src/errors.rs @@ -126,6 +126,12 @@ error_chain! { description("could not symlink directory") display("could not create link from '{}' to '{}'", src.display(), dest.display()) } + ReadingSymlink { + path: PathBuf, + } { + description("could not read symlink") + display("could not read symlink at '{}'", path.display()) + } CopyingDirectory { src: PathBuf, dest: PathBuf, diff --git a/src/elan-utils/src/utils.rs b/src/elan-utils/src/utils.rs index ca086f6..2db107e 100644 --- a/src/elan-utils/src/utils.rs +++ b/src/elan-utils/src/utils.rs @@ -1,6 +1,7 @@ use crate::errors::*; use crate::notifications::Notification; use dirs; +use json; use std::cmp::Ord; use std::env; use std::ffi::OsString; @@ -11,7 +12,6 @@ use std::process::Command; use url::Url; #[cfg(windows)] use winreg; -use json; use crate::raw; @@ -262,6 +262,25 @@ pub fn symlink_file(src: &Path, dest: &Path) -> Result<()> { .into()) } +// If we are on unix, we expect reading the symlink to work, +// and so return Some. +#[cfg(unix)] +pub fn read_link(path: &Path) -> Result> { + Ok(Some(::std::fs::read_link(path).chain_err(|| { + ErrorKind::ReadingSymlink { + path: PathBuf::from(path), + } + })?)) +} + +// If we are on windows, we don't attempt to read the symlink at all, +// and return None. This is not an error, but merely advice to the +// caller to not even attempt to show the symlink target. +#[cfg(windows)] +pub fn read_link(path: &Path) -> Result> { + Ok(None) +} + pub fn copy_dir(src: &Path, dest: &Path, notify_handler: &dyn Fn(Notification<'_>)) -> Result<()> { notify_handler(Notification::CopyingDirectory(src, dest)); raw::copy_dir(src, dest).chain_err(|| ErrorKind::CopyingDirectory { @@ -512,9 +531,10 @@ pub fn fetch_latest_release_json(url: &str, channel: &str, no_net: bool) -> Resu fetch_url(&url) }?; - let releases = json::parse(&json) - .chain_err(|| format!("failed to parse release data: {}", url))?; - releases[channel][0]["name"].as_str() + let releases = + json::parse(&json).chain_err(|| format!("failed to parse release data: {}", url))?; + releases[channel][0]["name"] + .as_str() .ok_or_else(|| format!("failed to parse release data: {}", url).into()) .map(|s| s.to_string()) } diff --git a/src/elan/toolchain.rs b/src/elan/toolchain.rs index b13fdcd..b421a3e 100644 --- a/src/elan/toolchain.rs +++ b/src/elan/toolchain.rs @@ -140,9 +140,10 @@ pub fn resolve_toolchain_desc_ext( utils::fetch_latest_release_json(uri, release, no_net) } else { if release == "beta" { - return Err(Error::from( - format!("channel 'beta' is not supported for custom origin '{}'", origin) - )); + return Err(Error::from(format!( + "channel 'beta' is not supported for custom origin '{}'", + origin + ))); } utils::fetch_latest_release_tag(origin, no_net) }; @@ -233,6 +234,9 @@ impl<'a> Toolchain<'a> { .map(|m| m.file_type().is_symlink()) .unwrap_or(false) } + pub fn symlink_target(&self) -> Result> { + Ok(utils::read_link(&self.path)?) + } pub fn exists(&self) -> bool { // HACK: linked toolchains are symlinks, and, contrary to what std docs // lead me to believe `fs::metadata`, used by `is_directory` does not