-
Notifications
You must be signed in to change notification settings - Fork 39
Expand file tree
/
Copy pathlib.rs
More file actions
166 lines (144 loc) · 5.26 KB
/
lib.rs
File metadata and controls
166 lines (144 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
use pet_core::os_environment::Environment;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub fn get_search_paths_from_env_variables(environment: &dyn Environment) -> Vec<PathBuf> {
let search_paths = environment
.get_know_global_search_locations()
.into_iter()
.map(normalize_search_path)
.collect::<HashSet<PathBuf>>();
// Exclude files from this folder, as they would have been discovered elsewhere (windows_store)
// Also the exe is merely a pointer to another file.
let user_home = environment.get_user_home();
search_paths
.into_iter()
.filter(|search_path| !is_windows_apps_path(search_path, user_home.as_ref()))
.collect()
}
fn is_windows_apps_path(search_path: &Path, user_home: Option<&PathBuf>) -> bool {
if let Some(home) = user_home {
let apps_path = home
.join("AppData")
.join("Local")
.join("Microsoft")
.join("WindowsApps");
if search_path.starts_with(apps_path) {
return true;
}
}
let components = search_path
.components()
.map(|component| component.as_os_str().to_string_lossy())
.collect::<Vec<_>>();
components.windows(4).any(|components| {
components[0].eq_ignore_ascii_case("AppData")
&& components[1].eq_ignore_ascii_case("Local")
&& components[2].eq_ignore_ascii_case("Microsoft")
&& components[3].eq_ignore_ascii_case("WindowsApps")
})
}
/// Normalizes a search path for deduplication purposes.
///
/// On Unix: Uses fs::canonicalize to resolve symlinks. This is important for merged-usr
/// systems where /bin, /sbin, /usr/sbin are symlinks to /usr/bin - we don't want to
/// report the same Python installation multiple times.
/// See: https://github.com/microsoft/python-environment-tools/pull/200
///
/// On Windows: Uses norm_case (GetLongPathNameW) to normalize case WITHOUT resolving
/// directory junctions. This is important for tools like Scoop that use junctions
/// (e.g., python\current -> python\3.13.3). Using fs::canonicalize would resolve
/// the junction, causing symlink tracking to fail when the shim points to the
/// junction path but executables are discovered from the resolved path.
/// See: https://github.com/microsoft/python-environment-tools/issues/187
fn normalize_search_path(path: PathBuf) -> PathBuf {
#[cfg(unix)]
{
std::fs::canonicalize(&path).unwrap_or(path)
}
#[cfg(windows)]
{
pet_fs::path::norm_case(&path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{
fs,
time::{SystemTime, UNIX_EPOCH},
};
struct TestEnvironment {
user_home: Option<PathBuf>,
global_search_locations: Vec<PathBuf>,
}
impl Environment for TestEnvironment {
fn get_user_home(&self) -> Option<PathBuf> {
self.user_home.clone()
}
fn get_root(&self) -> Option<PathBuf> {
None
}
fn get_env_var(&self, _key: String) -> Option<String> {
None
}
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
self.global_search_locations.clone()
}
}
fn create_test_dir(name: &str) -> PathBuf {
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let directory = std::env::temp_dir().join(format!(
"pet-env-var-path-{name}-{}-{unique}",
std::process::id()
));
fs::create_dir_all(&directory).unwrap();
directory
}
#[test]
fn search_paths_are_deduplicated_and_windows_apps_paths_are_filtered() {
let home = create_test_dir("home");
let regular_path = home.join("Python");
let windows_apps_path = home
.join("AppData")
.join("Local")
.join("Microsoft")
.join("WindowsApps");
fs::create_dir_all(®ular_path).unwrap();
fs::create_dir_all(&windows_apps_path).unwrap();
let environment = TestEnvironment {
user_home: Some(home.clone()),
global_search_locations: vec![
regular_path.clone(),
regular_path.clone(),
windows_apps_path,
],
};
let mut search_paths = get_search_paths_from_env_variables(&environment);
search_paths.sort();
assert_eq!(search_paths, vec![normalize_search_path(regular_path)]);
fs::remove_dir_all(home).unwrap();
}
#[test]
fn search_paths_are_preserved_when_home_is_unknown() {
let environment = TestEnvironment {
user_home: None,
global_search_locations: vec![
PathBuf::from("/usr/bin"),
PathBuf::from(if cfg!(windows) {
r"C:\Users\User\AppData\Local\Microsoft\WindowsApps"
} else {
"/Users/user/AppData/Local/Microsoft/WindowsApps"
}),
],
};
assert_eq!(
get_search_paths_from_env_variables(&environment),
vec![normalize_search_path(PathBuf::from("/usr/bin"))]
);
}
}