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(super) const BUILDDIR_TXT: &str = "pybuilddir.txt";
19 pub(super) const BUILD_LANDMARK: &str = "Modules/Setup.local";
20 pub(super) const VENV_LANDMARK: &str = "pyvenv.cfg";
21 pub(super) const BUILDSTDLIB_LANDMARK: &str = "Lib/os.py";
22
23 pub(super) fn stdlib_subdir() -> String {
24 format!("lib/python{}.{}", version::MAJOR, version::MINOR)
25 }
26
27 pub(super) fn stdlib_landmarks() -> [String; 2] {
28 let subdir = stdlib_subdir();
29 [format!("{subdir}/os.py"), format!("{subdir}/os.pyc")]
30 }
31
32 pub(super) fn platstdlib_landmark() -> String {
33 format!(
34 "lib/python{}.{}/lib-dynload",
35 version::MAJOR,
36 version::MINOR
37 )
38 }
39
40 pub(super) 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(super) const BUILDDIR_TXT: &str = "pybuilddir.txt";
50 pub(super) const BUILD_LANDMARK: &str = "Modules\\Setup.local";
51 pub(super) const VENV_LANDMARK: &str = "pyvenv.cfg";
52 pub(super) const BUILDSTDLIB_LANDMARK: &str = "Lib\\os.py";
53 pub(super) const STDLIB_SUBDIR: &str = "Lib";
54
55 pub(super) fn stdlib_landmarks() -> [String; 2] {
56 ["Lib\\os.py".into(), "Lib\\os.pyc".into()]
57 }
58
59 pub(super) fn platstdlib_landmark() -> String {
60 "DLLs".into()
61 }
62
63 pub(super) 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 Some(override_exe) = env_executable_override() {
122 paths.executable.clone_from(&override_exe);
123 paths.base_executable = real_executable;
124 PathBuf::from(&override_exe).parent().map(PathBuf::from)
125 } else {
126 paths.executable = real_executable;
127 executable
128 .as_ref()
129 .and_then(|p| p.parent().map(PathBuf::from))
130 };
131
132 let (venv_prefix, home_dir) = detect_venv(exe_dir.as_ref());
134 let search_dir = home_dir.clone().or(exe_dir);
135
136 let build_prefix = detect_build_directory(search_dir.as_ref());
138
139 let calculated_prefix = calculate_prefix(search_dir.as_ref(), build_prefix.as_ref());
142
143 if venv_prefix.is_some() {
145 paths.prefix = venv_prefix.as_ref().map_or_else(
147 || calculated_prefix.clone(),
148 |p| p.to_string_lossy().into_owned(),
149 );
150 paths.base_prefix = calculated_prefix;
151 } else {
152 paths.prefix.clone_from(&calculated_prefix);
154 paths.base_prefix = calculated_prefix;
155 }
156
157 paths.exec_prefix = if venv_prefix.is_some() {
159 paths.prefix.clone()
161 } else {
162 calculate_exec_prefix(search_dir.as_ref(), paths.prefix.as_ref())
163 };
164 paths.base_exec_prefix.clone_from(&paths.base_prefix);
165
166 if paths.base_executable.is_empty() {
168 paths.base_executable = calculate_base_executable(executable.as_ref(), home_dir.as_ref());
169 }
170
171 paths.module_search_paths =
173 build_module_search_paths(settings, &paths.prefix, &paths.exec_prefix);
174
175 paths.stdlib_dir = calculate_stdlib_dir(&paths.prefix);
177
178 paths
179}
180
181fn default_prefix(exe_dir: Option<&PathBuf>) -> String {
191 if let Some(prefix) = std::option_env!("RUSTPYTHON_PREFIX") {
192 return prefix.to_owned();
193 }
194
195 if cfg!(windows) {
196 if let Some(dir) = exe_dir {
197 return dir.to_string_lossy().into_owned();
198 }
199 "C:\\".to_owned()
202 } else {
203 "/usr/local".to_owned()
204 }
205}
206
207fn detect_venv(exe_dir: Option<&PathBuf>) -> (Option<PathBuf>, Option<PathBuf>) {
210 if let Some(dir) = exe_dir
212 && let Some(venv_dir) = dir.parent()
213 {
214 let cfg = venv_dir.join(platform::VENV_LANDMARK);
215 if cfg.exists()
216 && let Some(home) = parse_pyvenv_home(&cfg)
217 {
218 return (Some(venv_dir.to_path_buf()), Some(PathBuf::from(home)));
219 }
220 }
221
222 if let Some(dir) = exe_dir {
224 let cfg = dir.join(platform::VENV_LANDMARK);
225 if cfg.exists()
226 && let Some(home) = parse_pyvenv_home(&cfg)
227 {
228 return (Some(dir.clone()), Some(PathBuf::from(home)));
229 }
230 }
231
232 (None, None)
233}
234
235fn detect_build_directory(exe_dir: Option<&PathBuf>) -> Option<PathBuf> {
237 let dir = exe_dir?;
238
239 if dir.join(platform::BUILDDIR_TXT).exists() {
241 return Some(dir.clone());
242 }
243
244 if dir.join(platform::BUILD_LANDMARK).exists() {
246 return Some(dir.clone());
247 }
248
249 search_up_file(dir, &[platform::BUILDSTDLIB_LANDMARK])
251}
252
253fn calculate_prefix(exe_dir: Option<&PathBuf>, build_prefix: Option<&PathBuf>) -> String {
255 if let Some(bp) = build_prefix {
257 return bp.to_string_lossy().into_owned();
258 }
259
260 if let Some(dir) = exe_dir {
261 let zip = platform::zip_landmark();
263 if let Some(prefix) = search_up_file(dir, &[&zip]) {
264 return prefix.to_string_lossy().into_owned();
265 }
266
267 let landmarks = platform::stdlib_landmarks();
269 let refs: Vec<&str> = landmarks.iter().map(|s| s.as_str()).collect();
270 if let Some(prefix) = search_up_file(dir, &refs) {
271 return prefix.to_string_lossy().into_owned();
272 }
273 }
274
275 default_prefix(exe_dir)
277}
278
279fn calculate_exec_prefix(exe_dir: Option<&PathBuf>, prefix: &str) -> String {
281 #[cfg(windows)]
282 {
283 let _ = exe_dir; prefix.to_owned()
286 }
287
288 #[cfg(not(windows))]
289 {
290 if let Some(dir) = exe_dir {
292 let landmark = platform::platstdlib_landmark();
293 if let Some(exec_prefix) = search_up_dir(dir, &[&landmark]) {
294 return exec_prefix.to_string_lossy().into_owned();
295 }
296 }
297 prefix.to_owned()
299 }
300}
301
302fn calculate_base_executable(executable: Option<&PathBuf>, home_dir: Option<&PathBuf>) -> String {
304 if let (Some(exe), Some(home)) = (executable, home_dir)
306 && let Some(exe_name) = exe.file_name()
307 {
308 let base = home.join(exe_name);
309 return base.to_string_lossy().into_owned();
310 }
311
312 executable
314 .map(|p| p.to_string_lossy().into_owned())
315 .unwrap_or_default()
316}
317
318fn calculate_stdlib_dir(prefix: &str) -> Option<String> {
321 let stdlib_dir = Path::new(prefix).join(cfg_select! {
322 windows => platform::STDLIB_SUBDIR,
323 _ => platform::stdlib_subdir(),
324 });
325
326 if stdlib_dir.is_dir() {
327 Some(stdlib_dir.to_string_lossy().into_owned())
328 } else {
329 None
330 }
331}
332
333fn build_module_search_paths(settings: &Settings, prefix: &str, exec_prefix: &str) -> Vec<String> {
335 let mut paths = Vec::new();
336
337 paths.extend(settings.path_list.iter().cloned());
339
340 let zip_path = PathBuf::from(prefix).join(platform::zip_landmark());
342 paths.push(zip_path.to_string_lossy().into_owned());
343
344 #[cfg(not(windows))]
346 {
347 let stdlib_dir = PathBuf::from(prefix).join(platform::stdlib_subdir());
349 paths.push(stdlib_dir.to_string_lossy().into_owned());
350
351 let platstdlib = PathBuf::from(exec_prefix).join(platform::platstdlib_landmark());
352 paths.push(platstdlib.to_string_lossy().into_owned());
353 }
354
355 #[cfg(windows)]
356 {
357 let platstdlib = PathBuf::from(exec_prefix).join(platform::platstdlib_landmark());
359 paths.push(platstdlib.to_string_lossy().into_owned());
360
361 let stdlib_dir = PathBuf::from(prefix).join(platform::STDLIB_SUBDIR);
362 paths.push(stdlib_dir.to_string_lossy().into_owned());
363 }
364
365 paths
366}
367
368fn env_executable_override() -> Option<String> {
371 for name in ["PYTHONEXECUTABLE", "__PYVENV_LAUNCHER__"] {
372 if let Ok(value) = crate::host_env::os::var(name)
373 && !value.is_empty()
374 {
375 return Some(value);
376 }
377 }
378 None
379}
380
381fn get_executable_path() -> Option<PathBuf> {
383 #[cfg(not(target_arch = "wasm32"))]
384 {
385 let exec_arg = env::args_os().next()?;
386 crate::host_env::fs::which(exec_arg)
387 }
388 #[cfg(target_arch = "wasm32")]
389 {
390 let exec_arg = env::args().next()?;
391 Some(PathBuf::from(exec_arg))
392 }
393}
394
395fn parse_pyvenv_home(pyvenv_cfg: &Path) -> Option<String> {
397 #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
398 let content = crate::host_env::fs::read_to_string(pyvenv_cfg).ok()?;
399 #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
400 let content = std::fs::read_to_string(pyvenv_cfg).ok()?;
401
402 for line in content.lines() {
403 if let Some((key, value)) = line.split_once('=')
404 && key.trim().to_lowercase() == "home"
405 {
406 return Some(value.trim().to_string());
407 }
408 }
409
410 None
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn init_path_config_basic() {
419 let settings = Settings::default();
420 let paths = init_path_config(&settings);
421 assert!(!paths.prefix.is_empty());
423 }
424
425 #[test]
426 fn search_up() {
427 let result = search_up_file(
429 crate::host_env::os::temp_dir(),
430 &["nonexistent_landmark_xyz"],
431 );
432 assert!(result.is_none());
433 }
434
435 #[test]
436 fn default_prefix_basic() {
437 let prefix = default_prefix(None);
438 assert!(!prefix.is_empty());
439 }
440}