From 43d8d8620181ad2eb3b9dd6c2c5f555f61c02dc6 Mon Sep 17 00:00:00 2001 From: Tien Nguyen Date: Thu, 23 Jul 2026 13:24:14 -0400 Subject: [PATCH 1/2] make --find-interpreter skip prerelease Python unless --allow-prereleases --- src/build_context/builder.rs | 1 + src/build_options.rs | 8 +++- src/develop/mod.rs | 1 + src/python_interpreter/discovery.rs | 44 +++++++++++++++++++ .../get_interpreter_metadata.py | 1 + src/python_interpreter/mod.rs | 6 +++ src/python_interpreter/resolver.rs | 27 +++++++++++- 7 files changed, 85 insertions(+), 3 deletions(-) diff --git a/src/build_context/builder.rs b/src/build_context/builder.rs index 752c44172..37bb9f53c 100644 --- a/src/build_context/builder.rs +++ b/src/build_context/builder.rs @@ -340,6 +340,7 @@ impl BuildContextBuilder { metadata24.requires_python.as_ref(), &user_interpreters, build_options.python.find_interpreter, + build_options.python.allow_prereleases, has_import_lib_support, ); resolver.resolve() diff --git a/src/build_options.rs b/src/build_options.rs index a2a64e75c..5bec1d3fb 100644 --- a/src/build_options.rs +++ b/src/build_options.rs @@ -21,6 +21,11 @@ pub struct PythonOptions { #[arg(short = 'f', long, conflicts_with = "interpreter")] pub find_interpreter: bool, + /// Also build for prerelease (alpha/beta/rc) interpreters found by + /// `--find-interpreter`, which are skipped by default. + #[arg(long)] + pub allow_prereleases: bool, + /// Which kind of bindings to use. #[arg(short, long, value_parser = ["pyo3", "pyo3-ffi", "cffi", "uniffi", "bin"])] pub bindings: Option, @@ -594,7 +599,8 @@ mod tests { let bridge = BridgeModel::Cffi; let interpreter = vec![PathBuf::from("nonexistent-python-xyz")]; - let resolver = InterpreterResolver::new(&target, &bridge, None, &interpreter, false, false); + let resolver = + InterpreterResolver::new(&target, &bridge, None, &interpreter, false, false, false); let result = resolver.resolve(); let err_msg = result.unwrap_err().to_string(); assert_snapshot!(err_msg, @"Failed to find a python interpreter from `nonexistent-python-xyz`"); diff --git a/src/develop/mod.rs b/src/develop/mod.rs index 697ebebb2..ae0c412b7 100644 --- a/src/develop/mod.rs +++ b/src/develop/mod.rs @@ -359,6 +359,7 @@ pub fn develop(develop_options: DevelopOptions, venv_dir: &Path) -> Result<()> { python: PythonOptions { interpreter: vec![python.clone()], find_interpreter: false, + allow_prereleases: false, bindings, }, platform: PlatformOptions { diff --git a/src/python_interpreter/discovery.rs b/src/python_interpreter/discovery.rs index e4bcb738f..a11f9a11a 100644 --- a/src/python_interpreter/discovery.rs +++ b/src/python_interpreter/discovery.rs @@ -34,6 +34,7 @@ pub(super) struct InterpreterMetadataMessage { pub executable: Option, pub major: usize, pub minor: usize, + pub releaselevel: String, pub abiflags: Option, pub interpreter: String, pub ext_suffix: Option, @@ -680,6 +681,7 @@ fn from_metadata_message( runnable: true, implementation_name: message.implementation_name, soabi: message.soabi, + is_prerelease: message.releaselevel != "final", })) } @@ -877,6 +879,7 @@ mod tests { let message = |major, minor, platform: &str| InterpreterMetadataMessage { major, minor, + releaselevel: "final".to_string(), interpreter: "cpython".to_string(), implementation_name: "CPython".to_string(), abiflags: None, @@ -939,6 +942,7 @@ mod tests { runnable: true, implementation_name: "CPython".to_string(), soabi: None, + is_prerelease: false, } ); } @@ -984,6 +988,7 @@ mod tests { runnable: true, implementation_name: "CPython".to_string(), soabi: None, + is_prerelease: false, } ); } @@ -1004,6 +1009,7 @@ mod tests { let message_314 = InterpreterMetadataMessage { major: 3, minor: 14, + releaselevel: "final".to_string(), interpreter: "cpython".to_string(), implementation_name: "CPython".to_string(), abiflags: Some("".to_string()), @@ -1025,6 +1031,7 @@ mod tests { let message_314t = InterpreterMetadataMessage { major: 3, minor: 14, + releaselevel: "final".to_string(), interpreter: "cpython".to_string(), implementation_name: "CPython".to_string(), abiflags: Some("t".to_string()), @@ -1044,6 +1051,43 @@ mod tests { assert!(interp.gil_disabled); } + #[test] + fn test_interpreter_releaselevel_marks_prerelease() { + let target = Target::from_resolved_target_triple("x86_64-unknown-linux-gnu").unwrap(); + let bridge = BridgeModel::PyO3(PyO3 { + crate_name: PyO3Crate::PyO3, + version: semver::Version::new(0, 26, 0), + stable_abi: None, + metadata: None, + }); + let message = |releaselevel: &str| InterpreterMetadataMessage { + major: 3, + minor: 14, + releaselevel: releaselevel.to_string(), + interpreter: "cpython".to_string(), + implementation_name: "CPython".to_string(), + abiflags: Some("".to_string()), + ext_suffix: Some(".cpython-314-x86_64-linux-gnu.so".to_string()), + platform: "linux-x86_64".to_string(), + executable: None, + soabi: None, + gil_disabled: false, + system: "linux".to_string(), + }; + + for level in ["alpha", "beta", "candidate"] { + let interp = from_metadata_message("python3.14", &target, &bridge, message(level)) + .unwrap() + .unwrap(); + assert!(interp.is_prerelease, "{level} should be a prerelease"); + } + + let interp = from_metadata_message("python3.14", &target, &bridge, message("final")) + .unwrap() + .unwrap(); + assert!(!interp.is_prerelease, "final should not be a prerelease"); + } + #[test] fn test_check_executables_single_missing() { let target = Target::from_resolved_target_triple("x86_64-unknown-linux-gnu").unwrap(); diff --git a/src/python_interpreter/get_interpreter_metadata.py b/src/python_interpreter/get_interpreter_metadata.py index e2fd63d8d..6c8d7f7ea 100644 --- a/src/python_interpreter/get_interpreter_metadata.py +++ b/src/python_interpreter/get_interpreter_metadata.py @@ -26,6 +26,7 @@ "executable": sys.executable or None, "major": sys.version_info.major, "minor": sys.version_info.minor, + "releaselevel": sys.version_info.releaselevel, "abiflags": sysconfig.get_config_var("ABIFLAGS"), "interpreter": platform.python_implementation().lower(), "ext_suffix": ext_suffix, diff --git a/src/python_interpreter/mod.rs b/src/python_interpreter/mod.rs index dca016f40..c22dd88cb 100644 --- a/src/python_interpreter/mod.rs +++ b/src/python_interpreter/mod.rs @@ -105,6 +105,9 @@ pub struct PythonInterpreter { pub implementation_name: String, /// Comes from sysconfig var `SOABI` pub soabi: Option, + /// Whether this is a prerelease (alpha/beta/rc) interpreter, from + /// `sys.version_info.releaselevel`; `false` for non-probed placeholders + pub is_prerelease: bool, } impl Deref for PythonInterpreter { @@ -318,6 +321,7 @@ impl PythonInterpreter { runnable: false, implementation_name, soabi: None, + is_prerelease: false, } } @@ -346,6 +350,7 @@ impl PythonInterpreter { runnable: false, implementation_name: "cpython".to_string(), soabi: None, + is_prerelease: false, } } @@ -551,6 +556,7 @@ mod tests { runnable: false, implementation_name: kind.to_string().to_ascii_lowercase(), soabi: None, + is_prerelease: false, } } diff --git a/src/python_interpreter/resolver.rs b/src/python_interpreter/resolver.rs index bed9c31dd..641403865 100644 --- a/src/python_interpreter/resolver.rs +++ b/src/python_interpreter/resolver.rs @@ -193,6 +193,7 @@ pub struct InterpreterResolver<'a> { requires_python: Option<&'a VersionSpecifiers>, user_interpreters: &'a [PathBuf], find_interpreter: bool, + allow_prereleases: bool, has_import_lib_support: bool, } @@ -204,6 +205,7 @@ impl<'a> InterpreterResolver<'a> { requires_python: Option<&'a VersionSpecifiers>, user_interpreters: &'a [PathBuf], find_interpreter: bool, + allow_prereleases: bool, has_import_lib_support: bool, ) -> Self { Self { @@ -212,6 +214,7 @@ impl<'a> InterpreterResolver<'a> { requires_python, user_interpreters, find_interpreter, + allow_prereleases, has_import_lib_support, } } @@ -334,6 +337,7 @@ impl<'a> InterpreterResolver<'a> { runnable: false, implementation_name, soabi, + is_prerelease: false, }; Ok(( vec![Candidate { @@ -412,8 +416,26 @@ impl<'a> InterpreterResolver<'a> { fn discover_native(&self, fixed_abi3: Option<(u8, u8)>) -> Result { // --- Step 1+2: Find real interpreters with per-interpreter sysconfig fallback --- let found = if self.find_interpreter { - super::discovery::find_all(self.target, self.bridge, self.requires_python) - .context("Finding python interpreters failed")? + let found = super::discovery::find_all(self.target, self.bridge, self.requires_python) + .context("Finding python interpreters failed")?; + // Skip prereleases (alpha/beta/rc) unless opted in, so `--find-interpreter` + // doesn't silently build wheels for preview Python (e.g. published to PyPI). + if self.allow_prereleases { + found + } else { + found + .into_iter() + .filter(|interp| { + if interp.is_prerelease { + eprintln!( + "⚠️ Skipping prerelease interpreter {interp}; \ + pass `--allow-prereleases` to build for it" + ); + } + !interp.is_prerelease + }) + .collect() + } } else { self.find_specified_interpreters()? }; @@ -862,6 +884,7 @@ impl<'a> InterpreterResolver<'a> { runnable: false, implementation_name: interpreter_kind.to_string().to_ascii_lowercase(), soabi: soabi.cloned(), + is_prerelease: false, }) } From df0aeb35da53af8a9b31bb54ac45b84dc3983fc4 Mon Sep 17 00:00:00 2001 From: Tien Nguyen Date: Thu, 23 Jul 2026 14:16:31 -0400 Subject: [PATCH 2/2] fix failed tests --- tests/cmd/build.stdout | 4 ++++ tests/cmd/publish.stdout | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/tests/cmd/build.stdout b/tests/cmd/build.stdout index 1dd2a513c..af6fbbf36 100644 --- a/tests/cmd/build.stdout +++ b/tests/cmd/build.stdout @@ -33,6 +33,10 @@ Options: -f, --find-interpreter Find interpreters from the host machine + --allow-prereleases + Also build for prerelease (alpha/beta/rc) interpreters found by `--find-interpreter`, + which are skipped by default + -b, --bindings Which kind of bindings to use diff --git a/tests/cmd/publish.stdout b/tests/cmd/publish.stdout index 6351a1805..d1acc98aa 100644 --- a/tests/cmd/publish.stdout +++ b/tests/cmd/publish.stdout @@ -71,6 +71,10 @@ Options: -f, --find-interpreter Find interpreters from the host machine + --allow-prereleases + Also build for prerelease (alpha/beta/rc) interpreters found by `--find-interpreter`, + which are skipped by default + -b, --bindings Which kind of bindings to use