Honor standalone executable ._pth startup configuration

Apply explicit path isolation before CLI environment settings, preserve canonical empty-file and prefix handling, and retire three expected failures proven by test_site.

Assisted-by: OpenAI Codex: model version unavailable
This commit is contained in:
Codex committed 2026-10-28 14:34:09 +09:00
1 parent 0a183ee1c2
commit e1527c53f7
5 files changed
+150 -12

No files matched your search

-3
View File
@@ -741,7 +741,6 @@ class _pthFileTests(unittest.TestCase):
pth_lines.append('import site') pth_lines.append('import site')
return pth_lines return pth_lines
@unittest.expectedFailure # TODO: RUSTPYTHON
@support.requires_subprocess() @support.requires_subprocess()
def test_underpth_basic(self): def test_underpth_basic(self):
pth_lines = ['#.', '# ..', *sys.path, '.', '..'] pth_lines = ['#.', '# ..', *sys.path, '.', '..']
@@ -761,7 +760,6 @@ class _pthFileTests(unittest.TestCase):
"sys.path is incorrect" "sys.path is incorrect"
) )
@unittest.expectedFailure # TODO: RUSTPYTHON
@support.requires_subprocess() @support.requires_subprocess()
def test_underpth_nosite_file(self): def test_underpth_nosite_file(self):
libpath = test.support.STDLIB_DIR libpath = test.support.STDLIB_DIR
@@ -786,7 +784,6 @@ class _pthFileTests(unittest.TestCase):
"sys.path is incorrect" "sys.path is incorrect"
) )
@unittest.expectedFailure # TODO: RUSTPYTHON
@support.requires_subprocess() @support.requires_subprocess()
def test_underpth_file(self): def test_underpth_file(self):
libpath = test.support.STDLIB_DIR libpath = test.support.STDLIB_DIR
+130 -2
View File
@@ -7,6 +7,7 @@
use crate::vm::{Paths, Settings}; use crate::vm::{Paths, Settings};
use std::env; use std::env;
use std::io::{self, Read};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
// Platform-specific landmarks (ref: getpath.py PLATFORM CONSTANTS) // Platform-specific landmarks (ref: getpath.py PLATFORM CONSTANTS)
@@ -103,6 +104,127 @@ fn search_up_dir<P: AsRef<Path>>(start: P, landmarks: &[&str]) -> Option<PathBuf
// Path computation functions // Path computation functions
/// A detected ._pth file's effect on standalone CLI path configuration.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnderPth {
/// A zero-byte file changes home and ignores environment paths only.
Empty,
/// At least one input line replaces paths and enables isolated mode.
Isolated,
}
/// Discover standalone CLI ._pth configuration before reading environment options.
/// Embedded applications retain explicit Settings and do not perform this discovery.
pub fn configure_under_pth(settings: &mut Settings) -> io::Result<()> {
let Some(executable) = get_executable_path() else {
return Ok(());
};
#[cfg(unix)]
let resolved = resolve_executable_symlinks(&executable).filter(|path| *path != executable);
#[cfg(not(unix))]
let resolved: Option<PathBuf> = None;
for mut candidate in core::iter::once(executable).chain(resolved) {
if cfg!(windows)
&& candidate.extension().is_some_and(|ext| {
ext.as_encoded_bytes().eq_ignore_ascii_case(b"exe")
|| ext.as_encoded_bytes().eq_ignore_ascii_case(b"dll")
})
{
candidate.set_extension("");
}
candidate.as_mut_os_string().push("._pth");
#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))]
let file = crate::host_env::fs::open(&candidate);
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
let file = std::fs::File::open(&candidate);
let Ok(file) = file else { continue };
let mut content = Vec::new();
file.take(32 * 1024).read_to_end(&mut content)?;
if content.len() >= 32 * 1024 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"cannot read ._pth file of 32KB or larger during initialization",
));
}
// Settings currently stores paths as String, not surrogate-capable text.
// Reject unsupported encoding rather than changing paths or ignoring isolation.
let content = core::str::from_utf8(&content).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"non-UTF-8 ._pth paths are unsupported",
)
})?;
let parent = std::path::absolute(candidate.parent().unwrap_or_else(|| Path::new(".")))?;
let mut search_paths = Vec::new();
let mut import_site = false;
for line in content.lines() {
let line = line.split('#').next().unwrap_or_default().trim();
if line.is_empty() {
continue;
}
if line == "import site" {
import_site = true;
} else if line.starts_with("import ") {
eprintln!("unsupported 'import' line in ._pth file");
} else {
let joined = parent.join(line);
let double_root = cfg!(unix)
&& joined
.to_str()
.is_some_and(|path| path.starts_with("//") && !path.starts_with("///"));
let mut normalized = PathBuf::new();
for part in joined.components() {
match part {
std::path::Component::ParentDir => {
normalized.pop();
}
std::path::Component::CurDir => {}
_ => normalized.push(part.as_os_str()),
}
}
if double_root && let Ok(relative) = normalized.strip_prefix("/") {
normalized = Path::new("//").join(relative);
}
search_paths.push(
normalized
.to_str()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"non-UTF-8 ._pth paths are unsupported",
)
})?
.to_owned(),
);
}
}
let home = parent
.to_str()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"non-UTF-8 ._pth directory is unsupported",
)
})?
.to_owned();
settings.home = Some(home);
if content.is_empty() {
// getpath.py detects this file but skips its final `if pth:` block.
settings.under_pth = Some(UnderPth::Empty);
return Ok(());
}
settings.under_pth = Some(UnderPth::Isolated);
settings.path_list = search_paths;
settings.isolated = true;
settings.ignore_environment = true;
settings.user_site_directory = false;
settings.safe_path = true;
settings.import_site = import_site;
return Ok(());
}
Ok(())
}
/// Compute path configuration from Settings /// Compute path configuration from Settings
/// ///
/// This function should be called before interpreter initialization. /// This function should be called before interpreter initialization.
@@ -194,8 +316,11 @@ pub fn init_path_config(settings: &Settings) -> Paths {
if paths.base_executable.is_empty() { if paths.base_executable.is_empty() {
paths.base_executable = calculate_base_executable(executable.as_ref(), home_dir.as_ref()); paths.base_executable = calculate_base_executable(executable.as_ref(), home_dir.as_ref());
} }
paths.module_search_paths = paths.module_search_paths = if settings.under_pth == Some(UnderPth::Isolated) {
build_module_search_paths(settings, &paths.base_prefix, &paths.base_exec_prefix); settings.path_list.clone()
} else {
build_module_search_paths(settings, &paths.base_prefix, &paths.base_exec_prefix)
};
paths.stdlib_dir = calculate_stdlib_dir(&paths.base_prefix); paths.stdlib_dir = calculate_stdlib_dir(&paths.base_prefix);
paths paths
@@ -434,6 +559,9 @@ fn calculate_stdlib_dir(prefix: &str) -> Option<String> {
/// Preserve an embedding hook's custom search order by appending instead when /// Preserve an embedding hook's custom search order by appending instead when
/// the calculated paths have already been replaced or modified. /// the calculated paths have already been replaced or modified.
pub fn add_build_time_paths(settings: &Settings, paths: &mut Paths, extra: Vec<String>) { pub fn add_build_time_paths(settings: &Settings, paths: &mut Paths, extra: Vec<String>) {
if settings.under_pth.is_some() {
return;
}
let calculated = let calculated =
build_module_search_paths(settings, &paths.base_prefix, &paths.base_exec_prefix); build_module_search_paths(settings, &paths.base_prefix, &paths.base_exec_prefix);
let index = if paths.module_search_paths == calculated { let index = if paths.module_search_paths == calculated {
+4
View File
@@ -156,6 +156,9 @@ pub struct Settings {
/// Environment PYTHONPATH (and RUSTPYTHONPATH) /// Environment PYTHONPATH (and RUSTPYTHONPATH)
pub path_list: Vec<String>, pub path_list: Vec<String>,
/// Standalone CLI ._pth path override; embedded applications do not discover these files.
pub under_pth: Option<crate::getpath::UnderPth>,
/// PYTHONHOME: explicit prefix, optionally followed by the exec prefix. /// PYTHONHOME: explicit prefix, optionally followed by the exec prefix.
pub home: Option<String>, pub home: Option<String>,
// wchar_t *platlibdir; // wchar_t *platlibdir;
@@ -221,6 +224,7 @@ impl Default for Settings {
lazy_imports: -1, lazy_imports: -1,
warnoptions: vec![], warnoptions: vec![],
path_list: vec![], path_list: vec![],
under_pth: None,
home: None, home: None,
argv: vec![], argv: vec![],
hash_seed: None, hash_seed: None,
+2 -1
View File
@@ -101,7 +101,8 @@ fn setup_dynamic_stdlib(vm: &mut crate::VirtualMachine) {
// PYTHONHOME forbids falling back to the build-tree pure-Python library. // PYTHONHOME forbids falling back to the build-tree pure-Python library.
#[cfg(feature = "stdlib")] #[cfg(feature = "stdlib")]
fn use_build_stdlib(vm: &crate::VirtualMachine) -> bool { fn use_build_stdlib(vm: &crate::VirtualMachine) -> bool {
vm.state.config.paths.stdlib_dir.is_none() vm.state.config.settings.under_pth.is_none()
&& vm.state.config.paths.stdlib_dir.is_none()
&& vm.state.config.settings.home.as_deref().is_none_or(|home| { && vm.state.config.settings.home.as_deref().is_none_or(|home| {
let (prefix, _) = home let (prefix, _) = home
.split_once(if cfg!(windows) { ';' } else { ':' }) .split_once(if cfg!(windows) { ';' } else { ':' })
+14 -6
View File
@@ -217,14 +217,20 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> {
let (args, mode, argv) = parse_args()?; let (args, mode, argv) = parse_args()?;
let mut settings = Settings::default(); let mut settings = Settings::default();
settings.isolated = args.isolate; if let Err(err) = rustpython_vm::getpath::configure_under_pth(&mut settings) {
eprintln!("Fatal Python error: cannot configure ._pth paths: {err}");
std::process::exit(1);
}
settings.isolated |= args.isolate;
settings.ignore_environment = settings.isolated || args.ignore_environment; settings.ignore_environment = settings.isolated || args.ignore_environment;
settings.bytes_warning = args.bytes_warning.into(); settings.bytes_warning = args.bytes_warning.into();
settings.import_site = !args.no_site; if settings.under_pth != Some(rustpython_vm::getpath::UnderPth::Isolated) {
settings.import_site = !args.no_site;
}
let ignore_environment = settings.ignore_environment; let ignore_environment = settings.ignore_environment;
if !ignore_environment { if !ignore_environment && settings.under_pth.is_none() {
settings.path_list.extend(get_paths("RUSTPYTHONPATH")); settings.path_list.extend(get_paths("RUSTPYTHONPATH"));
settings.path_list.extend(get_paths("PYTHONPATH")); settings.path_list.extend(get_paths("PYTHONPATH"));
} }
@@ -233,9 +239,11 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> {
let get_env = |env| (!ignore_environment).then(|| env::var_os(env)).flatten(); let get_env = |env| (!ignore_environment).then(|| env::var_os(env)).flatten();
settings.home = get_env("PYTHONHOME") if settings.under_pth.is_none() {
.filter(|home| !home.is_empty()) settings.home = get_env("PYTHONHOME")
.map(|home| home.to_string_lossy().into_owned()); .filter(|home| !home.is_empty())
.map(|home| home.to_string_lossy().into_owned());
}
let env_count = |env| { let env_count = |env| {
get_env(env).filter(|v| !v.is_empty()).map_or(0, |val| { get_env(env).filter(|v| !v.is_empty()).map_or(0, |val| {