Skip to main content

rustpython_vm/
getpath.rs

1//! Path configuration for RustPython (ref: Modules/getpath.py)
2//!
3//! This module implements Python path calculation logic following getpath.py.
4//! It uses landmark-based search to locate prefix, exec_prefix, and stdlib directories.
5//!
6//! The main entry point is `init_path_config()` which computes Paths from Settings.
7
8use crate::vm::{Paths, Settings};
9use std::env;
10use std::path::{Path, PathBuf};
11
12// Platform-specific landmarks (ref: getpath.py PLATFORM CONSTANTS)
13
14#[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
68// Helper functions (ref: getpath.py HELPER FUNCTIONS)
69
70/// Search upward from a directory for landmark files/directories
71/// Returns the directory where a landmark was found
72fn 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
91/// Search upward for a file landmark
92fn search_up_file<P: AsRef<Path>>(start: P, landmarks: &[&str]) -> Option<PathBuf> {
93    search_up(start, landmarks, |p| p.is_file())
94}
95
96/// Search upward for a directory landmark
97#[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
102// Path computation functions
103
104/// Compute path configuration from Settings
105///
106/// This function should be called before interpreter initialization.
107/// It returns a Paths struct with all computed path values.
108pub fn init_path_config(settings: &Settings) -> Paths {
109    let mut paths = Paths::default();
110
111    // Step 0: Get executable path
112    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    // Step 1: Check for __PYVENV_LAUNCHER__ environment variable
119    // When launched from a venv launcher, __PYVENV_LAUNCHER__ contains the venv's python.exe path
120    // In this case:
121    //   - sys.executable should be the launcher path (where user invoked Python)
122    //   - sys._base_executable should be the real Python executable
123    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    // Step 2: Check for venv (pyvenv.cfg) and get 'home'
135    let (venv_prefix, home_dir) = detect_venv(&exe_dir);
136    let search_dir = home_dir.clone().or(exe_dir.clone());
137
138    // Step 3: Check for build directory
139    let build_prefix = detect_build_directory(&search_dir);
140
141    // Step 4: Calculate prefix via landmark search
142    // When in venv, search_dir is home_dir, so this gives us the base Python's prefix
143    let calculated_prefix = calculate_prefix(&search_dir, &build_prefix);
144
145    // Step 5: Set prefix and base_prefix
146    if venv_prefix.is_some() {
147        // In venv: prefix = venv directory, base_prefix = original Python's prefix
148        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        // Not in venv: prefix == base_prefix
155        paths.prefix = calculated_prefix.clone();
156        paths.base_prefix = calculated_prefix;
157    }
158
159    // Step 6: Calculate exec_prefix
160    paths.exec_prefix = if venv_prefix.is_some() {
161        // In venv: exec_prefix = prefix (venv directory)
162        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    // Step 7: Calculate base_executable (if not already set by __PYVENV_LAUNCHER__)
169    if paths.base_executable.is_empty() {
170        paths.base_executable = calculate_base_executable(executable.as_ref(), &home_dir);
171    }
172
173    // Step 8: Build module_search_paths
174    paths.module_search_paths =
175        build_module_search_paths(settings, &paths.prefix, &paths.exec_prefix);
176
177    // Step 9: Calculate stdlib_dir
178    paths.stdlib_dir = calculate_stdlib_dir(&paths.prefix);
179
180    paths
181}
182
183/// Get default prefix value
184fn 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
196/// Detect virtual environment by looking for pyvenv.cfg
197/// Returns (venv_prefix, home_dir from pyvenv.cfg)
198fn detect_venv(exe_dir: &Option<PathBuf>) -> (Option<PathBuf>, Option<PathBuf>) {
199    // Try exe_dir/../pyvenv.cfg first (standard venv layout: venv/bin/python)
200    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    // Try exe_dir/pyvenv.cfg (alternative layout)
212    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
224/// Detect if running from a build directory
225fn detect_build_directory(exe_dir: &Option<PathBuf>) -> Option<PathBuf> {
226    let dir = exe_dir.as_ref()?;
227
228    // Check for pybuilddir.txt (indicates build directory)
229    if dir.join(platform::BUILDDIR_TXT).exists() {
230        return Some(dir.clone());
231    }
232
233    // Check for Modules/Setup.local (build landmark)
234    if dir.join(platform::BUILD_LANDMARK).exists() {
235        return Some(dir.clone());
236    }
237
238    // Search up for Lib/os.py (build stdlib landmark)
239    search_up_file(dir, &[platform::BUILDSTDLIB_LANDMARK])
240}
241
242/// Calculate prefix by searching for landmarks
243fn calculate_prefix(exe_dir: &Option<PathBuf>, build_prefix: &Option<PathBuf>) -> String {
244    // 1. If build directory detected, use it
245    if let Some(bp) = build_prefix {
246        return bp.to_string_lossy().into_owned();
247    }
248
249    if let Some(dir) = exe_dir {
250        // 2. Search for ZIP landmark
251        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        // 3. Search for stdlib landmarks (os.py)
257        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    // 4. Fallback to default
265    default_prefix()
266}
267
268/// Calculate exec_prefix
269fn calculate_exec_prefix(exe_dir: &Option<PathBuf>, prefix: &str) -> String {
270    #[cfg(windows)]
271    {
272        // Windows: exec_prefix == prefix
273        let _ = exe_dir; // silence unused warning
274        prefix.to_owned()
275    }
276
277    #[cfg(not(windows))]
278    {
279        // POSIX: search for lib-dynload directory
280        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        // Fallback: same as prefix
287        prefix.to_owned()
288    }
289}
290
291/// Calculate base_executable
292fn calculate_base_executable(executable: Option<&PathBuf>, home_dir: &Option<PathBuf>) -> String {
293    // If in venv and we have home, construct base_executable from home
294    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    // Otherwise, base_executable == executable
302    executable
303        .map(|p| p.to_string_lossy().into_owned())
304        .unwrap_or_default()
305}
306
307/// Calculate stdlib_dir (sys._stdlib_dir)
308/// Returns None if the stdlib directory doesn't exist
309fn 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
323/// Build the complete module_search_paths (sys.path)
324fn build_module_search_paths(settings: &Settings, prefix: &str, exec_prefix: &str) -> Vec<String> {
325    let mut paths = Vec::new();
326
327    // 1. PYTHONPATH/RUSTPYTHONPATH from settings
328    paths.extend(settings.path_list.iter().cloned());
329
330    // 2. ZIP file path
331    let zip_path = PathBuf::from(prefix).join(platform::zip_landmark());
332    paths.push(zip_path.to_string_lossy().into_owned());
333
334    // 3. stdlib and platstdlib directories
335    #[cfg(not(windows))]
336    {
337        // POSIX: stdlib first, then lib-dynload
338        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        // Windows: DLLs first, then Lib
348        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
358/// Get the current executable path
359fn 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
372/// Parse pyvenv.cfg and extract the 'home' key value
373fn 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        // Just verify it doesn't panic and returns valid paths
396        assert!(!paths.prefix.is_empty());
397    }
398
399    #[test]
400    fn test_search_up() {
401        // Test with a path that doesn't have any landmarks
402        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}