Skip to content

Commit 19ae55a

Browse files
authored
fix: resolve symlinked /bin/python3 as LinuxGlobal (Fixes #405) (#413)
When `/bin` is a symlink to `/usr/bin` (common on modern Linux distros), `find_cached()` canonicalizes `/bin` → `/usr/bin` and only caches entries under `/usr/bin`. A subsequent `try_from` call with `/bin/python3` fails the direct cache lookup and returns `None`, causing the environment to be reported as `Unknown`. **Fix:** In `try_from`, after a cache miss, canonicalize the executable path and retry the lookup. When found via the canonical path, add the original path as a symlink and update both cache entries for consistency. - Canonicalize executable once, use for both path guard and cache fallback - Path guard accepts executables whose canonical path resolves to a known bin dir - Cache fallback adds the original path as a symlink for complete metadata - Both canonical and original cache entries updated to keep symlink lists consistent Fixes #405
1 parent 05d75c6 commit 19ae55a

File tree

1 file changed

+39
-6
lines changed
  • crates/pet-linux-global-python/src

1 file changed

+39
-6
lines changed

crates/pet-linux-global-python/src/lib.rs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,48 @@ impl Locator for LinuxGlobalPython {
8787

8888
self.find_cached(None);
8989

90-
// We only support python environments in /bin, /usr/bin, /usr/local/bin
91-
if !executable.starts_with("/bin")
92-
&& !executable.starts_with("/usr/bin")
93-
&& !executable.starts_with("/usr/local/bin")
94-
{
90+
// Resolve the canonical path once — used for both the path guard and cache fallback.
91+
let canonical = fs::canonicalize(&executable).ok();
92+
93+
// We only support python environments in /bin, /usr/bin, /usr/local/bin.
94+
// Check both the original and canonical paths so that symlinks from other
95+
// locations (e.g. /bin → /usr/bin) are still accepted.
96+
let dominated = |p: &Path| {
97+
p.starts_with("/bin") || p.starts_with("/usr/bin") || p.starts_with("/usr/local/bin")
98+
};
99+
if !dominated(&executable) && !canonical.as_ref().is_some_and(|c| dominated(c)) {
95100
return None;
96101
}
97102

98-
self.reported_executables.get(&executable)
103+
// Try direct cache lookup first.
104+
if let Some(env) = self.reported_executables.get(&executable) {
105+
return Some(env);
106+
}
107+
108+
// If the executable wasn't found directly, resolve symlinks and try the canonical path.
109+
// This handles cases like /bin/python3 → /usr/bin/python3 on systems where /bin
110+
// is a symlink to /usr/bin. The cache is populated using canonicalized bin directories,
111+
// so /bin/python3 won't be in the cache but /usr/bin/python3 will be.
112+
if let Some(canonical) = canonical {
113+
if canonical != executable {
114+
if let Some(mut env) = self.reported_executables.get(&canonical) {
115+
// Add the original path as a symlink so it's visible to consumers.
116+
let mut symlinks = env.symlinks.clone().unwrap_or_default();
117+
if !symlinks.contains(&executable) {
118+
symlinks.push(executable.clone());
119+
symlinks.sort();
120+
symlinks.dedup();
121+
env.symlinks = Some(symlinks);
122+
}
123+
// Update both the canonical and original entries for consistency.
124+
self.reported_executables.insert(canonical, env.clone());
125+
self.reported_executables.insert(executable, env.clone());
126+
return Some(env);
127+
}
128+
}
129+
}
130+
131+
None
99132
}
100133

101134
fn find(&self, reporter: &dyn Reporter) {

0 commit comments

Comments
 (0)