mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-29 03:04:38 +08:00
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:
1 parent
0a183ee1c2
commit
e1527c53f7
5 files changed
+150
-12
No files matched your search
Vendored
-3
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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| {
|
||||||
|
|||||||
Reference in new issue
Block a user