From c4681299c2ea0331394c871ea881f116d5cace15 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Mon, 19 Oct 2026 04:26:02 -0700 Subject: [PATCH] Honor Python 3.15 frozen-module startup configuration (#9202) * Honor frozen module startup selection Assisted-by: OpenAI Codex:model version unavailable * Match frozen option configuration diagnostics Assisted-by: OpenAI Codex:model version unavailable * Preserve frozen option initialization precedence Validate the first occurrence, then apply the final value as CPython does after getpath configuration normalization. Assisted-by: OpenAI Codex:model version unavailable --- crates/vm/src/stdlib/_imp.rs | 8 ++++++-- crates/vm/src/vm/mod.rs | 5 ++++- crates/vm/src/vm/setting.rs | 4 +++- crates/vm/src/vm/thread.rs | 1 + src/interpreter.rs | 9 +++++++-- src/settings.rs | 36 ++++++++++++++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/crates/vm/src/stdlib/_imp.rs b/crates/vm/src/stdlib/_imp.rs index 6d3a6b0b8..54357d63c 100644 --- a/crates/vm/src/stdlib/_imp.rs +++ b/crates/vm/src/stdlib/_imp.rs @@ -180,10 +180,14 @@ fn find_frozen(name: &str, vm: &VirtualMachine) -> Result 0 → true, override < 0 → false, 0 → default (true) + // use_frozen(): override > 0 → true, override < 0 → false, 0 → startup setting // When disabled, non-bootstrap modules are simply not found (same as look_up_frozen) let override_val = vm.state.override_frozen_modules.load(); - if override_val < 0 { + if override_val < 0 + || (override_val == 0 + && vm.import_bootstrap_complete + && !vm.state.config.settings.use_frozen_modules) + { return Err(FrozenError::NotFound); } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 6fd5107d4..0ddf79cc0 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -110,6 +110,7 @@ pub struct VirtualMachine { pub(crate) lazy_imports_resolving: RefCell>, pub state: PyRc, pub initialized: bool, + pub(crate) import_bootstrap_complete: bool, recursion_depth: Cell, /// Depth of native recursion that pushes no Python frame, counted only /// where the stack pointer cannot be read. Everywhere else the native @@ -1370,6 +1371,7 @@ impl VirtualMachine { lazy_imports_resolving: RefCell::default(), state, initialized: false, + import_bootstrap_complete: false, recursion_depth: Cell::new(0), #[cfg(any(miri, target_env = "musl"))] native_recursion_depth: Cell::new(0), @@ -1450,7 +1452,7 @@ impl VirtualMachine { // Use dotted names when freeze-stdlib is enabled (modules come from Lib/encodings/), // otherwise use underscored names (modules come from core_modules/). let (ascii_module_name, utf8_module_name, latin1_module_name) = - if cfg!(feature = "freeze-stdlib") { + if cfg!(feature = "freeze-stdlib") && self.state.config.settings.use_frozen_modules { ("encodings.ascii", "encodings.utf_8", "encodings.latin_1") } else { ("encodings_ascii", "encodings_utf_8", "encodings_latin_1") @@ -1644,6 +1646,7 @@ impl VirtualMachine { let res = essential_init(); let importlib = self.expect_pyresult(res, "essential initialization failed"); + self.import_bootstrap_complete = true; #[cfg(feature = "host_env")] if self.state.config.settings.allow_external_library diff --git a/crates/vm/src/vm/setting.rs b/crates/vm/src/vm/setting.rs index 458174910..c2a2c01ba 100644 --- a/crates/vm/src/vm/setting.rs +++ b/crates/vm/src/vm/setting.rs @@ -139,7 +139,8 @@ pub struct Settings { /// --check-hash-based-pycs pub check_hash_pycs_mode: CheckHashPycsMode, - // int use_frozen_modules; + /// -X frozen_modules=on|off (bootstrap modules remain available). + pub use_frozen_modules: bool, /// -P pub safe_path: bool, @@ -204,6 +205,7 @@ impl Default for Settings { install_signal_handlers: true, user_site_directory: true, import_site: true, + use_frozen_modules: true, ignore_environment: false, verbose: 0, quiet: false, diff --git a/crates/vm/src/vm/thread.rs b/crates/vm/src/vm/thread.rs index cbbc729b4..48dca83e6 100644 --- a/crates/vm/src/vm/thread.rs +++ b/crates/vm/src/vm/thread.rs @@ -1480,6 +1480,7 @@ impl VirtualMachine { lazy_imports_resolving: RefCell::default(), state: self.state.clone(), initialized: self.initialized, + import_bootstrap_complete: self.import_bootstrap_complete, recursion_depth: Cell::new(0), #[cfg(any(miri, target_env = "musl"))] native_recursion_depth: Cell::new(0), diff --git a/src/interpreter.rs b/src/interpreter.rs index c2ddf8cc5..303b0cb3c 100644 --- a/src/interpreter.rs +++ b/src/interpreter.rs @@ -55,6 +55,11 @@ fn install_default_tls_provider(_vm: &mut crate::VirtualMachine) { fn set_frozen_stdlib_dir(vm: &mut crate::VirtualMachine) { use rustpython_vm::common::rc::PyRc; + if !vm.state.config.settings.use_frozen_modules { + setup_dynamic_stdlib(vm); + return; + } + if use_build_stdlib(vm) { let state = PyRc::get_mut(&mut vm.state).unwrap(); state.config.paths.stdlib_dir = @@ -63,7 +68,7 @@ fn set_frozen_stdlib_dir(vm: &mut crate::VirtualMachine) { } /// Setup dynamic standard library loading from filesystem -#[cfg(all(feature = "stdlib", not(feature = "freeze-stdlib")))] +#[cfg(feature = "stdlib")] fn setup_dynamic_stdlib(vm: &mut crate::VirtualMachine) { use rustpython_vm::common::rc::PyRc; @@ -117,7 +122,7 @@ fn canonical_stdlib_path(path: String) -> String { } /// Collect standard library paths from build-time configuration -#[cfg(all(feature = "stdlib", not(feature = "freeze-stdlib")))] +#[cfg(feature = "stdlib")] fn collect_stdlib_paths() -> Vec { // BUILDTIME_RUSTPYTHONPATH should be set when distributing if let Some(paths) = option_env!("BUILDTIME_RUSTPYTHONPATH") { diff --git a/src/settings.rs b/src/settings.rs index af9cf4188..28606f4d9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -313,9 +313,15 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { settings.import_time = parse_import_time(value.to_str(), "PYTHONPROFILEIMPORTTIME"); } + if let Some(value) = get_env("PYTHON_FROZEN_MODULES").filter(|value| !value.is_empty()) { + settings.use_frozen_modules = + parse_frozen_modules(Some(&value.to_string_lossy()), "PYTHON_FROZEN_MODULES"); + } + // CPython uses the first occurrence of these -X options. let mut lazy_imports_option_seen = false; let mut import_time_option_seen = false; + let mut frozen_modules_option_seen = false; let xopts = args.implementation_option.into_iter().map(|s| { let (name, value) = match s.split_once('=') { Some((name, value)) => (name.to_owned(), Some(value)), @@ -327,6 +333,11 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { settings.import_time = parse_import_time(value, "-X importtime"); import_time_option_seen = true; } + "frozen_modules" if !frozen_modules_option_seen => { + settings.use_frozen_modules = + parse_frozen_modules(value, "option -X frozen_modules"); + frozen_modules_option_seen = true; + } "faulthandler" => settings.faulthandler = true, "warn_default_encoding" => settings.warn_default_encoding = true, "utf8" => { @@ -398,6 +409,18 @@ pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> { }); settings.xoptions.extend(xopts); + // CPython validates the first frozen_modules option before initialization, + // then getpath's config dictionary round-trip keeps the last occurrence. + if let Some((_, value)) = settings + .xoptions + .iter() + .rev() + .find(|(name, _)| name == "frozen_modules") + { + settings.use_frozen_modules = + parse_frozen_modules(value.as_deref(), "option -X frozen_modules"); + } + // Resolve utf8_mode if not explicitly set by PYTHONUTF8 or -X utf8. // Default to UTF-8 mode since RustPython's locale encoding detection // is incomplete. Users can set PYTHONUTF8=0 or -X utf8=0 to disable. @@ -513,6 +536,19 @@ fn parse_cpu_count(value: Option<&str>) -> Result, ()> { } } +fn parse_frozen_modules(value: Option<&str>, source: &str) -> bool { + match value { + None | Some("" | "on") => true, + Some("off") => false, + _ => { + eprintln!( + "Fatal Python error: config_init_import: bad value for {source} (expected \"on\" or \"off\")" + ); + std::process::exit(1); + } + } +} + /// = config_init_import_time fn parse_import_time(value: Option<&str>, source: &str) -> u8 { let value = value