Skip to content

Commit d4091d8

Browse files
committed
getpath
1 parent 46c61a6 commit d4091d8

File tree

5 files changed

+311
-119
lines changed

5 files changed

+311
-119
lines changed

crates/vm/src/getpath.rs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
//! Path configuration for RustPython (ref: Modules/getpath.py)
2+
//!
3+
//! This module provides path calculation logic but implemented directly in Rust.
4+
//!
5+
//! The main entry point is `init_path_config()` which should be called
6+
//! before interpreter initialization to populate Settings with path info.
7+
8+
use std::env;
9+
use std::path::{Path, PathBuf};
10+
11+
use crate::version;
12+
use crate::vm::Settings;
13+
14+
/// Initialize path configuration in Settings (like getpath.py)
15+
///
16+
/// This function should be called before interpreter initialization.
17+
/// It computes executable, base_executable, prefix, and module_search_paths.
18+
pub fn init_path_config(settings: &mut Settings) {
19+
// Skip if already configured
20+
if settings.module_search_paths_set {
21+
return;
22+
}
23+
24+
// 1. Compute executable path
25+
if settings.executable.is_none() {
26+
settings.executable = get_executable_path().map(|p| p.to_string_lossy().into_owned());
27+
}
28+
29+
let exe_path = settings
30+
.executable
31+
.as_ref()
32+
.map(PathBuf::from)
33+
.or_else(get_executable_path);
34+
35+
// 2. Compute base_executable (for venv support)
36+
if settings.base_executable.is_none() {
37+
settings.base_executable = compute_base_executable(exe_path.as_deref());
38+
}
39+
40+
// 3. Compute prefix paths
41+
let (prefix, base_prefix) = compute_prefixes(exe_path.as_deref());
42+
43+
if settings.prefix.is_none() {
44+
settings.prefix = prefix.clone();
45+
}
46+
if settings.base_prefix.is_none() {
47+
settings.base_prefix = base_prefix.clone().or_else(|| prefix.clone());
48+
}
49+
if settings.exec_prefix.is_none() {
50+
settings.exec_prefix = settings.prefix.clone();
51+
}
52+
if settings.base_exec_prefix.is_none() {
53+
settings.base_exec_prefix = settings.base_prefix.clone();
54+
}
55+
56+
// 4. Build module_search_paths
57+
settings.module_search_paths = compute_module_search_paths(settings, base_prefix.as_deref());
58+
settings.module_search_paths_set = true;
59+
}
60+
61+
/// Compute base_executable from executable path
62+
fn compute_base_executable(exe_path: Option<&Path>) -> Option<String> {
63+
let exe_path = exe_path?;
64+
65+
// Check for __PYVENV_LAUNCHER__ environment variable (like getpath.c env_to_dict)
66+
if let Ok(launcher) = env::var("__PYVENV_LAUNCHER__") {
67+
return Some(launcher);
68+
}
69+
70+
// Check if we're in a venv
71+
if let Some(venv_home) = get_venv_home(exe_path) {
72+
// venv_home is the bin directory containing the base Python
73+
let home_path = PathBuf::from(&venv_home);
74+
let exe_name = exe_path.file_name()?;
75+
let base_exe = home_path.join(exe_name);
76+
if base_exe.exists() {
77+
return Some(base_exe.to_string_lossy().into_owned());
78+
}
79+
// Fallback: just return the home directory path with exe name
80+
return Some(base_exe.to_string_lossy().into_owned());
81+
}
82+
83+
// Not in venv: base_executable == executable
84+
Some(exe_path.to_string_lossy().into_owned())
85+
}
86+
87+
/// Compute prefix and base_prefix from executable path
88+
fn compute_prefixes(exe_path: Option<&Path>) -> (Option<String>, Option<String>) {
89+
let Some(exe_path) = exe_path else {
90+
return (None, None);
91+
};
92+
93+
let exe_dir = match exe_path.parent() {
94+
Some(d) => d,
95+
None => return (None, None),
96+
};
97+
98+
// Check if we're in a venv
99+
if let Some(venv_home) = get_venv_home(exe_path) {
100+
// prefix is the venv directory (parent of bin/)
101+
let prefix = exe_dir.parent().map(|p| p.to_string_lossy().into_owned());
102+
103+
// base_prefix is parent of venv_home (the original Python's prefix)
104+
let home_path = PathBuf::from(&venv_home);
105+
let base_prefix = home_path.parent().map(|p| p.to_string_lossy().into_owned());
106+
107+
return (prefix, base_prefix);
108+
}
109+
110+
// Not in venv: prefix == base_prefix
111+
let prefix = exe_dir.parent().map(|p| p.to_string_lossy().into_owned());
112+
(prefix.clone(), prefix)
113+
}
114+
115+
/// Build the complete module_search_paths (sys.path)
116+
fn compute_module_search_paths(settings: &Settings, base_prefix: Option<&str>) -> Vec<String> {
117+
let mut paths = Vec::new();
118+
119+
// 1. Add paths from path_list (PYTHONPATH/RUSTPYTHONPATH)
120+
paths.extend(settings.path_list.iter().cloned());
121+
122+
// 2. Add zip stdlib path
123+
if let Some(base_prefix) = base_prefix {
124+
let platlibdir = "lib";
125+
let zip_name = format!("rustpython{}{}", version::MAJOR, version::MINOR);
126+
let zip_path = PathBuf::from(base_prefix).join(platlibdir).join(&zip_name);
127+
paths.push(zip_path.to_string_lossy().into_owned());
128+
}
129+
130+
paths
131+
}
132+
133+
/// Get the zip stdlib path to add to sys.path
134+
///
135+
/// Returns a path like `/usr/local/lib/rustpython313` or
136+
/// `/path/to/venv/lib/rustpython313` for virtual environments.
137+
pub fn get_zip_stdlib_path() -> Option<String> {
138+
// ZIP_LANDMARK pattern: {platlibdir}/{impl_name}{VERSION_MAJOR}{VERSION_MINOR}
139+
let platlibdir = "lib";
140+
let zip_name = format!("rustpython{}{}", version::MAJOR, version::MINOR);
141+
142+
let base_prefix = get_base_prefix()?;
143+
let zip_path = base_prefix.join(platlibdir).join(&zip_name);
144+
145+
Some(zip_path.to_string_lossy().into_owned())
146+
}
147+
148+
/// Get the base prefix directory
149+
///
150+
/// For installed Python: parent of the bin directory
151+
/// For venv: the 'home' value from pyvenv.cfg
152+
fn get_base_prefix() -> Option<PathBuf> {
153+
let exe_path = get_executable_path()?;
154+
let exe_dir = exe_path.parent()?;
155+
156+
// Check if we're in a venv by looking for pyvenv.cfg
157+
if let Some(venv_home) = get_venv_home(&exe_path) {
158+
// venv_home is the directory containing the base Python
159+
// Go up one level to get the prefix (e.g., /usr/local from /usr/local/bin)
160+
let home_path = PathBuf::from(&venv_home);
161+
if let Some(parent) = home_path.parent() {
162+
return Some(parent.to_path_buf());
163+
}
164+
return Some(home_path);
165+
}
166+
167+
// Not in venv: go up from bin/ to get prefix
168+
// e.g., /usr/local/bin/rustpython -> /usr/local
169+
exe_dir.parent().map(|p| p.to_path_buf())
170+
}
171+
172+
/// Get the current executable path
173+
fn get_executable_path() -> Option<PathBuf> {
174+
#[cfg(not(target_arch = "wasm32"))]
175+
{
176+
let exec_arg = env::args_os().next()?;
177+
which::which(exec_arg).ok()
178+
}
179+
#[cfg(target_arch = "wasm32")]
180+
{
181+
let exec_arg = env::args().next()?;
182+
Some(PathBuf::from(exec_arg))
183+
}
184+
}
185+
186+
/// Get the 'home' value from pyvenv.cfg if running in a virtual environment
187+
///
188+
/// pyvenv.cfg is located in the parent directory of the bin directory
189+
/// (e.g., venv/pyvenv.cfg when executable is venv/bin/rustpython)
190+
pub fn get_venv_home(exe_path: &Path) -> Option<String> {
191+
let exe_dir = exe_path.parent()?;
192+
let venv_dir = exe_dir.parent()?;
193+
let pyvenv_cfg = venv_dir.join("pyvenv.cfg");
194+
195+
if !pyvenv_cfg.exists() {
196+
return None;
197+
}
198+
199+
parse_pyvenv_home(&pyvenv_cfg)
200+
}
201+
202+
/// Parse pyvenv.cfg and extract the 'home' key value
203+
fn parse_pyvenv_home(pyvenv_cfg: &Path) -> Option<String> {
204+
let content = std::fs::read_to_string(pyvenv_cfg).ok()?;
205+
206+
for line in content.lines() {
207+
if let Some((key, value)) = line.split_once('=')
208+
&& key.trim().to_lowercase() == "home"
209+
{
210+
return Some(value.trim().to_string());
211+
}
212+
}
213+
214+
None
215+
}
216+
217+
#[cfg(test)]
218+
mod tests {
219+
use super::*;
220+
221+
#[test]
222+
fn test_zip_stdlib_path_format() {
223+
// Just verify it returns something and doesn't panic
224+
let _path = get_zip_stdlib_path();
225+
}
226+
}

crates/vm/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ pub mod exceptions;
6060
pub mod format;
6161
pub mod frame;
6262
pub mod function;
63+
pub mod getpath;
6364
pub mod import;
6465
mod intern;
6566
pub mod iter;

0 commit comments

Comments
 (0)