Skip to content

Commit 3def20f

Browse files
authored
fix: canonicalize prefix paths before comparison to handle symlinked venvs (Fixes #358) (#359)
Fix false positive "Prefix is incorrect" telemetry warning when a venv prefix path is a symlink. - Canonicalize both discovered and resolved prefix paths before comparison so symlinks to the same directory are treated as equal - Wrap canonicalized paths in `norm_case()` to handle Windows `\\?\` UNC prefix from `canonicalize` - Fall back to original path if canonicalization fails (e.g., path no longer exists) Fixes #358
1 parent 1a405f5 commit 3def20f

File tree

3 files changed

+129
-6
lines changed

3 files changed

+129
-6
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pet-telemetry/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ log = "0.4.21"
1515
env_logger = "0.10.2"
1616
lazy_static = "1.4.0"
1717
regex = "1.10.4"
18+
19+
[dev-dependencies]
20+
tempfile = "3.10"

crates/pet-telemetry/src/lib.rs

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub fn report_inaccuracies_identified_after_resolving(
2121
_reporter: &dyn Reporter,
2222
env: &PythonEnvironment,
2323
resolved: &PythonEnvironment,
24-
) -> Option<()> {
24+
) -> Option<InaccuratePythonEnvironmentInfo> {
2525
let known_symlinks = env.symlinks.clone().unwrap_or_default();
2626
let resolved_executable = &resolved.executable.clone()?;
2727
let norm_cased_executable = norm_case(resolved_executable);
@@ -39,10 +39,21 @@ pub fn report_inaccuracies_identified_after_resolving(
3939
executable_not_in_symlinks = false;
4040
}
4141

42-
let mut invalid_prefix = env.prefix.clone().unwrap_or_default() != resolved.prefix.clone()?;
43-
if env.prefix.clone().is_none() {
44-
invalid_prefix = false;
45-
}
42+
let invalid_prefix = if let Some(ref env_prefix) = env.prefix {
43+
let resolved_prefix = resolved.prefix.clone()?;
44+
// Canonicalize both paths to handle symlinks — a venv prefix like
45+
// /usr/local/venvs/myvenv may be a symlink to /usr/local/venvs/versioned/myvenv-1.0.51,
46+
// and sys.prefix returns the resolved target. Without this, the comparison
47+
// produces a false positive "Prefix is incorrect" warning. (See #358)
48+
// Wrap in norm_case to handle Windows UNC prefix (`\\?\`) from canonicalize.
49+
let env_canonical =
50+
norm_case(std::fs::canonicalize(env_prefix).unwrap_or(env_prefix.clone()));
51+
let resolved_canonical =
52+
norm_case(std::fs::canonicalize(&resolved_prefix).unwrap_or(resolved_prefix));
53+
env_canonical != resolved_canonical
54+
} else {
55+
false
56+
};
4657

4758
let mut invalid_arch = env.arch.clone() != resolved.arch.clone();
4859
if env.arch.clone().is_none() {
@@ -73,8 +84,9 @@ pub fn report_inaccuracies_identified_after_resolving(
7384
env, resolved, event
7485
);
7586
// reporter.report_telemetry(TelemetryEvent::InaccuratePythonEnvironmentInfo(event));
87+
return Some(event);
7688
}
77-
Option::Some(())
89+
None
7890
}
7991

8092
fn are_versions_different(actual: &str, expected: &str) -> Option<bool> {
@@ -84,3 +96,110 @@ fn are_versions_different(actual: &str, expected: &str) -> Option<bool> {
8496
let expected = expected.get(1)?.as_str().to_string();
8597
Some(actual != expected)
8698
}
99+
100+
#[cfg(test)]
101+
mod tests {
102+
use super::*;
103+
use pet_core::{
104+
manager::EnvManager,
105+
python_environment::{PythonEnvironmentBuilder, PythonEnvironmentKind},
106+
telemetry::TelemetryEvent,
107+
};
108+
use std::path::PathBuf;
109+
110+
struct NoopReporter;
111+
impl Reporter for NoopReporter {
112+
fn report_manager(&self, _: &EnvManager) {}
113+
fn report_environment(&self, _: &PythonEnvironment) {}
114+
fn report_telemetry(&self, _: &TelemetryEvent) {}
115+
}
116+
117+
fn make_env(
118+
executable: PathBuf,
119+
prefix: PathBuf,
120+
version: &str,
121+
symlinks: Vec<PathBuf>,
122+
) -> PythonEnvironment {
123+
PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Venv))
124+
.executable(Some(executable))
125+
.prefix(Some(prefix))
126+
.version(Some(version.to_string()))
127+
.symlinks(Some(symlinks))
128+
.build()
129+
}
130+
131+
#[test]
132+
fn same_prefix_is_not_flagged() {
133+
let dir = tempfile::tempdir().unwrap();
134+
let prefix = dir.path().to_path_buf();
135+
let exe = prefix.join("bin").join("python");
136+
137+
let env = make_env(exe.clone(), prefix.clone(), "3.12.7", vec![exe.clone()]);
138+
let resolved = make_env(exe.clone(), prefix, "3.12.7", vec![exe]);
139+
140+
let result = report_inaccuracies_identified_after_resolving(&NoopReporter, &env, &resolved);
141+
assert!(result.is_none(), "identical prefixes should not be flagged");
142+
}
143+
144+
#[cfg(unix)]
145+
#[test]
146+
fn symlinked_prefix_is_not_flagged() {
147+
let dir = tempfile::tempdir().unwrap();
148+
let real_prefix = dir.path().join("versioned").join("myvenv-1.0.51");
149+
std::fs::create_dir_all(&real_prefix).unwrap();
150+
let symlink_prefix = dir.path().join("myvenv");
151+
std::os::unix::fs::symlink(&real_prefix, &symlink_prefix).unwrap();
152+
153+
let exe = symlink_prefix.join("bin").join("python");
154+
155+
// Discovery sees the symlink path as prefix
156+
let env = make_env(exe.clone(), symlink_prefix, "3.12.7", vec![exe.clone()]);
157+
// Resolution (spawning Python) returns the canonical path
158+
let resolved = make_env(exe.clone(), real_prefix, "3.12.7", vec![exe]);
159+
160+
let result = report_inaccuracies_identified_after_resolving(&NoopReporter, &env, &resolved);
161+
assert!(
162+
result.is_none(),
163+
"symlinked prefix to the same directory should not be flagged"
164+
);
165+
}
166+
167+
#[test]
168+
fn different_prefix_is_flagged() {
169+
let dir = tempfile::tempdir().unwrap();
170+
let prefix_a = dir.path().join("env_a");
171+
let prefix_b = dir.path().join("env_b");
172+
std::fs::create_dir_all(&prefix_a).unwrap();
173+
std::fs::create_dir_all(&prefix_b).unwrap();
174+
175+
let exe = prefix_a.join("bin").join("python");
176+
177+
let env = make_env(exe.clone(), prefix_a, "3.12.7", vec![exe.clone()]);
178+
let resolved = make_env(exe.clone(), prefix_b, "3.12.7", vec![exe]);
179+
180+
let result = report_inaccuracies_identified_after_resolving(&NoopReporter, &env, &resolved);
181+
let event = result.expect("genuinely different prefixes should be flagged");
182+
assert_eq!(event.invalid_prefix, Some(true));
183+
}
184+
185+
#[test]
186+
fn none_prefix_is_not_flagged() {
187+
let dir = tempfile::tempdir().unwrap();
188+
let prefix = dir.path().to_path_buf();
189+
let exe = prefix.join("bin").join("python");
190+
191+
// env has no prefix
192+
let env = PythonEnvironmentBuilder::new(Some(PythonEnvironmentKind::Venv))
193+
.executable(Some(exe.clone()))
194+
.version(Some("3.12.7".to_string()))
195+
.symlinks(Some(vec![exe.clone()]))
196+
.build();
197+
let resolved = make_env(exe.clone(), prefix, "3.12.7", vec![exe]);
198+
199+
let result = report_inaccuracies_identified_after_resolving(&NoopReporter, &env, &resolved);
200+
assert!(
201+
result.is_none(),
202+
"None prefix should not cause any inaccuracy flag"
203+
);
204+
}
205+
}

0 commit comments

Comments
 (0)