1use crate::vm::{Paths, Settings};
9use std::env;
10use std::path::{Path, PathBuf};
11
12#[cfg(not(windows))]
15mod platform {
16 use crate::version;
17
18 pub const BUILDDIR_TXT: &str = "pybuilddir.txt";
19 pub const BUILD_LANDMARK: &str = "Modules/Setup.local";
20 pub const VENV_LANDMARK: &str = "pyvenv.cfg";
21 pub const BUILDSTDLIB_LANDMARK: &str = "Lib/os.py";
22
23 pub fn stdlib_subdir() -> String {
24 format!("lib/python{}.{}", version::MAJOR, version::MINOR)
25 }
26
27 pub fn stdlib_landmarks() -> [String; 2] {
28 let subdir = stdlib_subdir();
29 [format!("{}/os.py", subdir), format!("{}/os.pyc", subdir)]
30 }
31
32 pub fn platstdlib_landmark() -> String {
33 format!(
34 "lib/python{}.{}/lib-dynload",
35 version::MAJOR,
36 version::MINOR
37 )
38 }
39
40 pub fn zip_landmark() -> String {
41 format!("lib/python{}{}.zip", version::MAJOR, version::MINOR)
42 }
43}
44
45#[cfg(windows)]
46mod platform {
47 use crate::version;
48
49 pub const BUILDDIR_TXT: &str = "pybuilddir.txt";
50 pub const BUILD_LANDMARK: &str = "Modules\\Setup.local";
51 pub const VENV_LANDMARK: &str = "pyvenv.cfg";
52 pub const BUILDSTDLIB_LANDMARK: &str = "Lib\\os.py";
53 pub const STDLIB_SUBDIR: &str = "Lib";
54
55 pub fn stdlib_landmarks() -> [String; 2] {
56 ["Lib\\os.py".into(), "Lib\\os.pyc".into()]
57 }
58
59 pub fn platstdlib_landmark() -> String {
60 "DLLs".into()
61 }
62
63 pub fn zip_landmark() -> String {
64 format!("python{}{}.zip", version::MAJOR, version::MINOR)
65 }
66}
67
68fn search_up<P, F>(start: P, landmarks: &[&str], test: F) -> Option<PathBuf>
73where
74 P: AsRef<Path>,
75 F: Fn(&Path) -> bool,
76{
77 let mut current = start.as_ref().to_path_buf();
78 loop {
79 for landmark in landmarks {
80 let path = current.join(landmark);
81 if test(&path) {
82 return Some(current);
83 }
84 }
85 if !current.pop() {
86 return None;
87 }
88 }
89}
90
91fn search_up_file<P: AsRef<Path>>(start: P, landmarks: &[&str]) -> Option<PathBuf> {
93 search_up(start, landmarks, |p| p.is_file())
94}
95
96#[cfg(not(windows))]
98fn search_up_dir<P: AsRef<Path>>(start: P, landmarks: &[&str]) -> Option<PathBuf> {
99 search_up(start, landmarks, |p| p.is_dir())
100}
101
102pub fn init_path_config(settings: &Settings) -> Paths {
109 let mut paths = Paths::default();
110
111 let executable = get_executable_path();
113 let real_executable = executable
114 .as_ref()
115 .map(|p| p.to_string_lossy().into_owned())
116 .unwrap_or_default();
117
118 let exe_dir = if let Ok(launcher) = env::var("__PYVENV_LAUNCHER__") {
124 paths.executable = launcher.clone();
125 paths.base_executable = real_executable;
126 PathBuf::from(&launcher).parent().map(PathBuf::from)
127 } else {
128 paths.executable = real_executable;
129 executable
130 .as_ref()
131 .and_then(|p| p.parent().map(PathBuf::from))
132 };
133
134 let (venv_prefix, home_dir) = detect_venv(&exe_dir);
136 let search_dir = home_dir.clone().or(exe_dir.clone());
137
138 let build_prefix = detect_build_directory(&search_dir);
140
141 let calculated_prefix = calculate_prefix(&search_dir, &build_prefix);
144
145 if venv_prefix.is_some() {
147 paths.prefix = venv_prefix
149 .as_ref()
150 .map(|p| p.to_string_lossy().into_owned())
151 .unwrap_or_else(|| calculated_prefix.clone());
152 paths.base_prefix = calculated_prefix;
153 } else {
154 paths.prefix = calculated_prefix.clone();
156 paths.base_prefix = calculated_prefix;
157 }
158
159 paths.exec_prefix = if venv_prefix.is_some() {
161 paths.prefix.clone()
163 } else {
164 calculate_exec_prefix(&search_dir, &paths.prefix)
165 };
166 paths.base_exec_prefix = paths.base_prefix.clone();
167
168 if paths.base_executable.is_empty() {
170 paths.base_executable = calculate_base_executable(executable.as_ref(), &home_dir);
171 }
172
173 paths.module_search_paths =
175 build_module_search_paths(settings, &paths.prefix, &paths.exec_prefix);
176
177 paths.stdlib_dir = calculate_stdlib_dir(&paths.prefix);
179
180 paths
181}
182
183fn default_prefix() -> String {
185 std::option_env!("RUSTPYTHON_PREFIX")
186 .map(String::from)
187 .unwrap_or_else(|| {
188 if cfg!(windows) {
189 "C:".to_owned()
190 } else {
191 "/usr/local".to_owned()
192 }
193 })
194}
195
196fn detect_venv(exe_dir: &Option<PathBuf>) -> (Option<PathBuf>, Option<PathBuf>) {
199 if let Some(dir) = exe_dir
201 && let Some(venv_dir) = dir.parent()
202 {
203 let cfg = venv_dir.join(platform::VENV_LANDMARK);
204 if cfg.exists()
205 && let Some(home) = parse_pyvenv_home(&cfg)
206 {
207 return (Some(venv_dir.to_path_buf()), Some(PathBuf::from(home)));
208 }
209 }
210
211 if let Some(dir) = exe_dir {
213 let cfg = dir.join(platform::VENV_LANDMARK);
214 if cfg.exists()
215 && let Some(home) = parse_pyvenv_home(&cfg)
216 {
217 return (Some(dir.clone()), Some(PathBuf::from(home)));
218 }
219 }
220
221 (None, None)
222}
223
224fn detect_build_directory(exe_dir: &Option<PathBuf>) -> Option<PathBuf> {
226 let dir = exe_dir.as_ref()?;
227
228 if dir.join(platform::BUILDDIR_TXT).exists() {
230 return Some(dir.clone());
231 }
232
233 if dir.join(platform::BUILD_LANDMARK).exists() {
235 return Some(dir.clone());
236 }
237
238 search_up_file(dir, &[platform::BUILDSTDLIB_LANDMARK])
240}
241
242fn calculate_prefix(exe_dir: &Option<PathBuf>, build_prefix: &Option<PathBuf>) -> String {
244 if let Some(bp) = build_prefix {
246 return bp.to_string_lossy().into_owned();
247 }
248
249 if let Some(dir) = exe_dir {
250 let zip = platform::zip_landmark();
252 if let Some(prefix) = search_up_file(dir, &[&zip]) {
253 return prefix.to_string_lossy().into_owned();
254 }
255
256 let landmarks = platform::stdlib_landmarks();
258 let refs: Vec<&str> = landmarks.iter().map(|s| s.as_str()).collect();
259 if let Some(prefix) = search_up_file(dir, &refs) {
260 return prefix.to_string_lossy().into_owned();
261 }
262 }
263
264 default_prefix()
266}
267
268fn calculate_exec_prefix(exe_dir: &Option<PathBuf>, prefix: &str) -> String {
270 #[cfg(windows)]
271 {
272 let _ = exe_dir; prefix.to_owned()
275 }
276
277 #[cfg(not(windows))]
278 {
279 if let Some(dir) = exe_dir {
281 let landmark = platform::platstdlib_landmark();
282 if let Some(exec_prefix) = search_up_dir(dir, &[&landmark]) {
283 return exec_prefix.to_string_lossy().into_owned();
284 }
285 }
286 prefix.to_owned()
288 }
289}
290
291fn calculate_base_executable(executable: Option<&PathBuf>, home_dir: &Option<PathBuf>) -> String {
293 if let (Some(exe), Some(home)) = (executable, home_dir)
295 && let Some(exe_name) = exe.file_name()
296 {
297 let base = home.join(exe_name);
298 return base.to_string_lossy().into_owned();
299 }
300
301 executable
303 .map(|p| p.to_string_lossy().into_owned())
304 .unwrap_or_default()
305}
306
307fn calculate_stdlib_dir(prefix: &str) -> Option<String> {
310 #[cfg(not(windows))]
311 let stdlib_dir = PathBuf::from(prefix).join(platform::stdlib_subdir());
312
313 #[cfg(windows)]
314 let stdlib_dir = PathBuf::from(prefix).join(platform::STDLIB_SUBDIR);
315
316 if stdlib_dir.is_dir() {
317 Some(stdlib_dir.to_string_lossy().into_owned())
318 } else {
319 None
320 }
321}
322
323fn build_module_search_paths(settings: &Settings, prefix: &str, exec_prefix: &str) -> Vec<String> {
325 let mut paths = Vec::new();
326
327 paths.extend(settings.path_list.iter().cloned());
329
330 let zip_path = PathBuf::from(prefix).join(platform::zip_landmark());
332 paths.push(zip_path.to_string_lossy().into_owned());
333
334 #[cfg(not(windows))]
336 {
337 let stdlib_dir = PathBuf::from(prefix).join(platform::stdlib_subdir());
339 paths.push(stdlib_dir.to_string_lossy().into_owned());
340
341 let platstdlib = PathBuf::from(exec_prefix).join(platform::platstdlib_landmark());
342 paths.push(platstdlib.to_string_lossy().into_owned());
343 }
344
345 #[cfg(windows)]
346 {
347 let platstdlib = PathBuf::from(exec_prefix).join(platform::platstdlib_landmark());
349 paths.push(platstdlib.to_string_lossy().into_owned());
350
351 let stdlib_dir = PathBuf::from(prefix).join(platform::STDLIB_SUBDIR);
352 paths.push(stdlib_dir.to_string_lossy().into_owned());
353 }
354
355 paths
356}
357
358fn get_executable_path() -> Option<PathBuf> {
360 #[cfg(not(target_arch = "wasm32"))]
361 {
362 let exec_arg = env::args_os().next()?;
363 which::which(exec_arg).ok()
364 }
365 #[cfg(target_arch = "wasm32")]
366 {
367 let exec_arg = env::args().next()?;
368 Some(PathBuf::from(exec_arg))
369 }
370}
371
372fn parse_pyvenv_home(pyvenv_cfg: &Path) -> Option<String> {
374 let content = std::fs::read_to_string(pyvenv_cfg).ok()?;
375
376 for line in content.lines() {
377 if let Some((key, value)) = line.split_once('=')
378 && key.trim().to_lowercase() == "home"
379 {
380 return Some(value.trim().to_string());
381 }
382 }
383
384 None
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_init_path_config() {
393 let settings = Settings::default();
394 let paths = init_path_config(&settings);
395 assert!(!paths.prefix.is_empty());
397 }
398
399 #[test]
400 fn test_search_up() {
401 let result = search_up_file(std::env::temp_dir(), &["nonexistent_landmark_xyz"]);
403 assert!(result.is_none());
404 }
405
406 #[test]
407 fn test_default_prefix() {
408 let prefix = default_prefix();
409 assert!(!prefix.is_empty());
410 }
411}