From c8e40b9c5060464224703eefd65c13940ca9fb14 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 30 Mar 2026 12:14:53 -0600 Subject: [PATCH 01/20] add CPythonABI enum for pyo3-build-config InterpreterConfig --- pyo3-build-config/src/impl_.rs | 244 +++++++++++++++++++++------------ pyo3-build-config/src/lib.rs | 10 +- pyo3-ffi/build.rs | 14 +- 3 files changed, 166 insertions(+), 102 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 3d64cb34e4c..9a51bc50ae3 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -44,7 +44,7 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { }; /// Maximum Python version that can be used as minimum required Python version with abi3. -pub(crate) const ABI3_MAX_MINOR: u8 = 14; +pub(crate) const STABLE_ABI_MAX_MINOR: u8 = 14; #[cfg(test)] thread_local! { @@ -83,6 +83,42 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum CPythonABI { + ABI3, + VersionSpecific, +} + +impl CPythonABI { + fn from_build_env() -> Result { + match is_abi3() { + true => Ok(CPythonABI::ABI3), + false => Ok(CPythonABI::VersionSpecific), + } + } +} + +impl Display for CPythonABI { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CPythonABI::ABI3 => write!(f, "abi3"), + CPythonABI::VersionSpecific => write!(f, "version_specific"), + } + } +} + +impl FromStr for CPythonABI { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + match value { + "abi3" => Ok(CPythonABI::ABI3), + "version_specific" => Ok(CPythonABI::VersionSpecific), + _ => Err(format!("Unrecognized ABI name: {value}").into()), + } + } +} + /// Configuration needed by PyO3 to build for the correct Python implementation. /// /// Usually this is queried directly from the Python interpreter, or overridden using the @@ -109,8 +145,8 @@ pub struct InterpreterConfig { /// Whether linking against the stable/limited Python 3 API. /// - /// Serialized to `abi3`. - pub abi3: bool, + /// Serialized to `stable_abi`. + pub stable_abi: CPythonABI, /// The name of the link library defining Python. /// @@ -194,9 +230,13 @@ impl InterpreterConfig { PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - // If Py_GIL_DISABLED is set, do not build with limited API support - if self.abi3 && !self.is_free_threaded() { - out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + match self.stable_abi { + CPythonABI::ABI3 => { + if !self.is_free_threaded() { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + } + } + CPythonABI::VersionSpecific => {} } for flag in &self.build_flags.0 { @@ -310,7 +350,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse minor version")?, }; - let abi3 = is_abi3(); + let stable_abi = CPythonABI::from_build_env()?; let implementation = map["implementation"].parse()?; @@ -327,7 +367,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) default_lib_name_windows( version, implementation, - abi3, + stable_abi, map["mingw"].as_str() == "True", // This is the best heuristic currently available to detect debug build // on Windows from sysconfig - e.g. ext_suffix may be @@ -339,7 +379,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) default_lib_name_unix( version, implementation, - abi3, + stable_abi, cygwin, map.get("ld_version").map(String::as_str), gil_disabled, @@ -366,7 +406,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) version, implementation, shared, - abi3, + stable_abi, lib_name: Some(lib_name), lib_dir, executable: map.get("executable").cloned(), @@ -421,11 +461,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let abi3 = is_abi3(); + let stable_abi = CPythonABI::from_build_env()?; let lib_name = Some(default_lib_name_unix( version, implementation, - abi3, + stable_abi, cygwin, sysconfigdata.get_value("LDVERSION"), gil_disabled, @@ -439,7 +479,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) implementation, version, shared: shared || framework, - abi3, + stable_abi, lib_dir, lib_name, executable: None, @@ -473,7 +513,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) // // TODO: abi3 is a property of the build mode, not the interpreter. Should this be // removed from `InterpreterConfig`? - config.abi3 |= is_abi3(); + config.stable_abi = CPythonABI::from_build_env()?; config.fixup_for_abi3_version(get_abi3_version())?; Ok(config) @@ -516,7 +556,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut implementation = None; let mut version = None; let mut shared = None; - let mut abi3 = None; + let mut stable_abi = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -541,7 +581,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "implementation" => parse_value!(implementation, value), "version" => parse_value!(version, value), "shared" => parse_value!(shared, value), - "abi3" => parse_value!(abi3, value), + "stable_abi" => parse_value!(stable_abi, value), "lib_name" => parse_value!(lib_name, value), "lib_dir" => parse_value!(lib_dir, value), "executable" => parse_value!(executable, value), @@ -560,14 +600,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); - let abi3 = abi3.unwrap_or(false); + let stable_abi = stable_abi.unwrap_or(CPythonABI::VersionSpecific); let build_flags = build_flags.unwrap_or_default(); Ok(InterpreterConfig { implementation, version, shared: shared.unwrap_or(true), - abi3, + stable_abi, lib_name, lib_dir, executable, @@ -590,7 +630,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) self.lib_name = Some(default_lib_name_for_target( self.version, self.implementation, - self.abi3, + self.stable_abi, self.is_free_threaded(), target, )); @@ -645,7 +685,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) write_line!(implementation)?; write_line!(version)?; write_line!(shared)?; - write_line!(abi3)?; + write_line!(stable_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -707,7 +747,18 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) return Ok(()); } - if let Some(version) = abi3_version { + self.fixup_for_stable_abi_version(abi3_version, is_abi3)?; + + Ok(()) + } + + /// Core logic for pinning a Python stable ABI version to minimum and maximum supported versions + fn fixup_for_stable_abi_version( + &mut self, + abi_version: Option, + abi_check: impl Fn() -> bool, + ) -> Result<()> { + if let Some(version) = abi_version { ensure!( version <= self.version, "cannot set a minimum Python version {} higher than the interpreter version {} \ @@ -716,11 +767,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) self.version, version.minor, ); - self.version = version; - } else if is_abi3() && self.version.minor > ABI3_MAX_MINOR { - warn!("Automatically falling back to abi3-py3{ABI3_MAX_MINOR} because current Python is higher than the maximum supported"); - self.version.minor = ABI3_MAX_MINOR; + } else if abi_check() && self.version.minor > STABLE_ABI_MAX_MINOR { + warn!("Automatically falling back to abi3-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); + self.version.minor = STABLE_ABI_MAX_MINOR; } Ok(()) @@ -853,7 +903,7 @@ fn is_abi3() -> bool { /// /// Must be called from a PyO3 crate build script. pub fn get_abi3_version() -> Option { - let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR) + let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=STABLE_ABI_MAX_MINOR) .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{i}")).is_some()); minor_version.map(|minor| PythonVersion { major: 3, minor }) } @@ -1568,7 +1618,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Result Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let implementation = PythonImplementation::CPython; - let abi3 = true; + let stable_abi = CPythonABI::ABI3; let lib_name = if host.operating_system == OperatingSystem::Windows { Some(default_lib_name_windows( version, implementation, - abi3, + stable_abi, false, false, false, @@ -1631,7 +1681,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result String { if target.operating_system == OperatingSystem::Windows { - default_lib_name_windows(version, implementation, abi3, false, false, gil_disabled).unwrap() + default_lib_name_windows( + version, + implementation, + stable_abi, + false, + false, + gil_disabled, + ) + .unwrap() } else { default_lib_name_unix( version, implementation, - abi3, + stable_abi, target.operating_system == OperatingSystem::Cygwin, None, gil_disabled, @@ -1703,7 +1761,7 @@ fn default_lib_name_for_target( fn default_lib_name_windows( version: PythonVersion, implementation: PythonImplementation, - abi3: bool, + stable_abi: CPythonABI, mingw: bool, debug: bool, gil_disabled: bool, @@ -1717,11 +1775,13 @@ fn default_lib_name_windows( // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 Ok(format!("python{}{}_d", version.major, version.minor)) - } else if abi3 && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) { + } else if stable_abi != CPythonABI::VersionSpecific + && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) + { if debug { - Ok(WINDOWS_ABI3_DEBUG_LIB_NAME.to_owned()) + Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) } else { - Ok(WINDOWS_ABI3_LIB_NAME.to_owned()) + Ok(WINDOWS_STABLE_ABI_LIB_NAME.to_owned()) } } else if mingw { ensure!( @@ -1747,7 +1807,7 @@ fn default_lib_name_windows( fn default_lib_name_unix( version: PythonVersion, implementation: PythonImplementation, - abi3: bool, + stable_abi: CPythonABI, cygwin: bool, ld_version: Option<&str>, gil_disabled: bool, @@ -1756,7 +1816,7 @@ fn default_lib_name_unix( PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && abi3 { + if cygwin && stable_abi != CPythonABI::VersionSpecific { Ok("python3".to_string()) } else if gil_disabled { ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); @@ -1922,7 +1982,7 @@ pub fn make_interpreter_config() -> Result { let abi3_version = get_abi3_version(); // See if we can safely skip the Python interpreter configuration detection. - // Unix "abi3" extension modules can usually be built without any interpreter. + // Unix stable ABI extension modules can usually be built without any interpreter. let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); if have_python_interpreter() { @@ -1931,7 +1991,7 @@ pub fn make_interpreter_config() -> Result { // Bail if the interpreter configuration is required to build. Err(e) if need_interpreter => return Err(e), _ => { - // Fall back to the "abi3" defaults just as if `PYO3_NO_PYTHON` + // Fall back to the stable ABI just as if `PYO3_NO_PYTHON` // environment variable was set. warn!("Compiling without a working Python interpreter."); } @@ -1990,7 +2050,7 @@ mod tests { #[test] fn test_config_file_roundtrip() { let config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::ABI3, build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), @@ -2011,7 +2071,7 @@ mod tests { // And some different options, for variety let config = InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: { let mut flags = HashSet::new(); flags.insert(BuildFlag::Py_DEBUG); @@ -2041,7 +2101,7 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { let config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), @@ -2071,7 +2131,7 @@ mod tests { version: PythonVersion { major: 3, minor: 8 }, implementation: PythonImplementation::CPython, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2094,7 +2154,7 @@ mod tests { version: PythonVersion { major: 3, minor: 8 }, implementation: PythonImplementation::CPython, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2194,7 +2254,7 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, @@ -2224,7 +2284,7 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, @@ -2251,7 +2311,7 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, @@ -2278,7 +2338,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, shared: true, - abi3: true, + stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -2302,7 +2362,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: true, + stable_abi: CPythonABI::ABI3, lib_name: None, lib_dir: None, executable: None, @@ -2337,7 +2397,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python38".into()), lib_dir: Some("C:\\some\\path".into()), executable: None, @@ -2372,7 +2432,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python38".into()), lib_dir: Some("/usr/lib/mingw".into()), executable: None, @@ -2407,7 +2467,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3.9".into()), lib_dir: Some("/usr/arm64/lib".into()), executable: None, @@ -2444,7 +2504,7 @@ mod tests { minor: 11 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("pypy3.11-c".into()), lib_dir: None, executable: None, @@ -2459,12 +2519,13 @@ mod tests { #[test] fn default_lib_name_windows() { + use CPythonABI::*; use PythonImplementation::*; assert_eq!( super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, false, false, @@ -2475,7 +2536,7 @@ mod tests { assert!(super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2485,7 +2546,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, false, false, @@ -2497,7 +2558,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, true, false, false, @@ -2509,7 +2570,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, true, false, false, @@ -2521,7 +2582,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, PyPy, - true, + ABI3, false, false, false, @@ -2536,7 +2597,7 @@ mod tests { minor: 11 }, PyPy, - false, + ABI3, false, false, false, @@ -2548,7 +2609,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - false, + ABI3, false, true, false, @@ -2562,7 +2623,7 @@ mod tests { super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, CPython, - true, + ABI3, false, true, false, @@ -2577,7 +2638,7 @@ mod tests { minor: 10 }, CPython, - true, + ABI3, false, true, false, @@ -2592,7 +2653,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, false, true, @@ -2605,7 +2666,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, true, false, true, @@ -2618,7 +2679,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, false, true, @@ -2633,7 +2694,7 @@ mod tests { minor: 13 }, CPython, - true, // abi3 true should not affect the free-threaded lib name + ABI3, // abi3 true should not affect the free-threaded lib name false, false, true, @@ -2648,7 +2709,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, true, true, @@ -2660,13 +2721,14 @@ mod tests { #[test] fn default_lib_name_unix() { + use CPythonABI::*; use PythonImplementation::*; // Defaults to pythonX.Y for CPython 3.8+ assert_eq!( super::default_lib_name_unix( PythonVersion { major: 3, minor: 8 }, CPython, - false, + VersionSpecific, false, None, false @@ -2678,7 +2740,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, None, false @@ -2691,7 +2753,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - false, + VersionSpecific, false, Some("3.8d"), false @@ -2708,7 +2770,7 @@ mod tests { minor: 11 }, PyPy, - false, + VersionSpecific, false, None, false @@ -2721,7 +2783,7 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, PyPy, - false, + VersionSpecific, false, Some("3.11d"), false @@ -2738,7 +2800,7 @@ mod tests { minor: 13 }, CPython, - false, + VersionSpecific, false, None, true @@ -2753,7 +2815,7 @@ mod tests { minor: 12, }, CPython, - false, + VersionSpecific, false, None, true, @@ -2767,7 +2829,7 @@ mod tests { minor: 13 }, CPython, - true, + ABI3, true, None, false @@ -2831,7 +2893,7 @@ mod tests { #[test] fn interpreter_version_reduced_to_abi3() { let mut config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::ABI3, build_flags: BuildFlags::default(), pointer_width: None, executable: None, @@ -2855,7 +2917,7 @@ mod tests { #[test] fn abi3_version_cannot_be_higher_than_interpreter() { let mut config = InterpreterConfig { - abi3: true, + stable_abi: CPythonABI::ABI3, build_flags: BuildFlags::new(), pointer_width: None, executable: None, @@ -2920,7 +2982,7 @@ mod tests { assert_eq!( parsed_config, InterpreterConfig { - abi3: false, + stable_abi: CPythonABI::VersionSpecific, build_flags: BuildFlags(interpreter_config.build_flags.0.clone()), pointer_width: Some(64), executable: None, @@ -3058,7 +3120,7 @@ mod tests { minor: 11, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3100,7 +3162,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: true, + stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3146,7 +3208,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3179,7 +3241,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 8 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3234,7 +3296,7 @@ mod tests { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -3297,7 +3359,7 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.abi3 = true; + config.stable_abi = CPythonABI::ABI3; config.lib_name = None; config.apply_default_lib_name_to_config_file(&unix); assert_eq!(config.lib_name, Some("python3.13".into())); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 9bc9a9c8cc9..e0a1d5070f5 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -268,13 +268,13 @@ pub fn print_expected_cfgs() { // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) - for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::STABLE_ABI_MAX_MINOR + 1 { println!("cargo:rustc-check-cfg=cfg(Py_3_{i})"); } // pyo3_dll cfg for raw-dylib linking on Windows let mut dll_names = vec!["python3".to_string(), "python3_d".to_string()]; - for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::STABLE_ABI_MAX_MINOR + 1 { dll_names.push(format!("python3{i}")); dll_names.push(format!("python3{i}_d")); if i >= 13 { @@ -311,7 +311,7 @@ pub mod pyo3_build_script_impl { } pub use crate::impl_::{ cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config, - target_triple_from_env, InterpreterConfig, PythonVersion, + target_triple_from_env, CPythonABI, InterpreterConfig, PythonVersion, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. @@ -491,7 +491,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -534,7 +534,7 @@ mod tests { minor: 13, }, shared: true, - abi3: false, + stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 7c9be0e4cb0..eeacbb31d15 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -2,7 +2,7 @@ use pyo3_build_config::{ bail, ensure, print_feature_cfgs, pyo3_build_script_impl::{ cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target, - resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, + resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, CPythonABI, InterpreterConfig, MaximumVersionExceeded, PythonVersion, }, warn, PythonImplementation, @@ -67,10 +67,12 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { ); } else if interpreter_config.version > v_plus_1 { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); + let major = interpreter_config.version.major; + let minor = interpreter_config.version.minor; if interpreter_config.is_free_threaded() { - error.add_help( - "the free-threaded build of CPython does not support the limited API so this check cannot be suppressed.", - ); + error.add_help(&format!( + "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", + )); return Err(error.finish().into()); } @@ -125,12 +127,12 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if interpreter_config.abi3 { + if let CPythonABI::ABI3 = interpreter_config.stable_abi { match interpreter_config.implementation { PythonImplementation::CPython => { if interpreter_config.is_free_threaded() { warn!( - "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." + "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." ) } } From 45d5cc107625727336b1cce49b2f040d9b20c53e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 30 Mar 2026 12:24:07 -0600 Subject: [PATCH 02/20] add release note --- newsfragments/5924.changed.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 newsfragments/5924.changed.md diff --git a/newsfragments/5924.changed.md b/newsfragments/5924.changed.md new file mode 100644 index 00000000000..07d2d59c8f6 --- /dev/null +++ b/newsfragments/5924.changed.md @@ -0,0 +1,2 @@ +The boolean abi3 field of pyo3_build_config::impl_::InterpreterConfig is now a +variant of a new CPythonABI enum. From e3dc13345d82d1150493a38a95fff8a1975087a3 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 15 Apr 2026 16:35:25 -0600 Subject: [PATCH 03/20] store ABI details in a new PythonAbi struct --- pyo3-build-config/src/impl_.rs | 918 +++++++++++++++----------------- pyo3-build-config/src/lib.rs | 24 +- pyo3-ffi-check/macro/src/lib.rs | 5 +- pyo3-ffi/build.rs | 45 +- 4 files changed, 465 insertions(+), 527 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9a51bc50ae3..32c1b942ada 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -83,42 +83,6 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum CPythonABI { - ABI3, - VersionSpecific, -} - -impl CPythonABI { - fn from_build_env() -> Result { - match is_abi3() { - true => Ok(CPythonABI::ABI3), - false => Ok(CPythonABI::VersionSpecific), - } - } -} - -impl Display for CPythonABI { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - CPythonABI::ABI3 => write!(f, "abi3"), - CPythonABI::VersionSpecific => write!(f, "version_specific"), - } - } -} - -impl FromStr for CPythonABI { - type Err = crate::errors::Error; - - fn from_str(value: &str) -> Result { - match value { - "abi3" => Ok(CPythonABI::ABI3), - "version_specific" => Ok(CPythonABI::VersionSpecific), - _ => Err(format!("Unrecognized ABI name: {value}").into()), - } - } -} - /// Configuration needed by PyO3 to build for the correct Python implementation. /// /// Usually this is queried directly from the Python interpreter, or overridden using the @@ -128,26 +92,17 @@ impl FromStr for CPythonABI { /// strategies are used to populate this type. #[cfg_attr(test, derive(Debug, PartialEq, Eq))] pub struct InterpreterConfig { - /// The Python implementation flavor. - /// - /// Serialized to `implementation`. - pub implementation: PythonImplementation, - - /// Python `X.Y` version. e.g. `3.9`. + /// Which abi the build is configured to link against /// - /// Serialized to `version`. - pub version: PythonVersion, + /// Serialized to `abi`. + /// See the documentation for the PythonAbi enum for more details. + pub abi: PythonAbi, /// Whether link library is shared. /// /// Serialized to `shared`. pub shared: bool, - /// Whether linking against the stable/limited Python 3 API. - /// - /// Serialized to `stable_abi`. - pub stable_abi: CPythonABI, - /// The name of the link library defining Python. /// /// This effectively controls the `cargo:rustc-link-lib=` value to @@ -215,28 +170,28 @@ impl InterpreterConfig { #[doc(hidden)] pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. - assert!(self.version >= MINIMUM_SUPPORTED_VERSION); + assert!(self.abi.version >= MINIMUM_SUPPORTED_VERSION); let mut out = vec![]; - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version.minor { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.abi.version.minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } - match self.implementation { + match self.abi.implementation { PythonImplementation::CPython => {} PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()), PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - match self.stable_abi { - CPythonABI::ABI3 => { - if !self.is_free_threaded() { + match self.abi.kind { + PythonAbiKind::Abi3 => { + if !self.abi.kind.is_free_threaded() { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } } - CPythonABI::VersionSpecific => {} + PythonAbiKind::VersionSpecific(_) => {} } for flag in &self.build_flags.0 { @@ -252,7 +207,10 @@ impl InterpreterConfig { } #[doc(hidden)] - pub fn from_interpreter(interpreter: impl AsRef) -> Result { + pub fn from_interpreter( + interpreter: impl AsRef, + abi3_version: Option, + ) -> Result { const SCRIPT: &str = r#" # Allow the script to run on Python 2, so that nicer error can be printed later. from __future__ import print_function @@ -350,8 +308,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse minor version")?, }; - let stable_abi = CPythonABI::from_build_env()?; - let implementation = map["implementation"].parse()?; let gil_disabled = match map["gil_disabled"].as_str() { @@ -361,29 +317,28 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; + let mut abi_builder = + PythonAbiBuilder::new(implementation, version).adjust_from_build_env(abi3_version)?; + + if gil_disabled { + abi_builder = abi_builder.free_threaded()?; + } + + let abi = abi_builder.finalize(); + let cygwin = map["cygwin"].as_str() == "True"; let lib_name = if cfg!(windows) { default_lib_name_windows( - version, - implementation, - stable_abi, + abi, map["mingw"].as_str() == "True", // This is the best heuristic currently available to detect debug build // on Windows from sysconfig - e.g. ext_suffix may be // `_d.cp312-win_amd64.pyd` for 3.12 debug build map["ext_suffix"].starts_with("_d."), - gil_disabled, )? } else { - default_lib_name_unix( - version, - implementation, - stable_abi, - cygwin, - map.get("ld_version").map(String::as_str), - gil_disabled, - )? + default_lib_name_unix(abi, cygwin, map.get("ld_version").map(String::as_str))? }; let lib_dir = if cfg!(windows) { @@ -403,10 +358,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse calcsize_pointer")?; Ok(InterpreterConfig { - version, - implementation, shared, - stable_abi, + abi, lib_name: Some(lib_name), lib_dir, executable: map.get("executable").cloned(), @@ -461,14 +414,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let stable_abi = CPythonABI::from_build_env()?; + let mut abi_builder = + PythonAbiBuilder::new(implementation, version).adjust_from_build_env(None)?; + if gil_disabled { + abi_builder = abi_builder.free_threaded()?; + } + let abi = abi_builder.finalize(); let lib_name = Some(default_lib_name_unix( - version, - implementation, - stable_abi, + abi, cygwin, sysconfigdata.get_value("LDVERSION"), - gil_disabled, )?); let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") .map(|bytes_width: u32| bytes_width * 8) @@ -476,10 +431,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); Ok(InterpreterConfig { - implementation, - version, + abi, shared: shared || framework, - stable_abi, lib_dir, lib_name, executable: None, @@ -510,11 +463,13 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse contents of PYO3_CONFIG_FILE")?; // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 // feature. - // - // TODO: abi3 is a property of the build mode, not the interpreter. Should this be - // removed from `InterpreterConfig`? - config.stable_abi = CPythonABI::from_build_env()?; - config.fixup_for_abi3_version(get_abi3_version())?; + let mut abi_builder = + PythonAbiBuilder::new(config.abi.implementation, config.abi.version) + .adjust_from_build_env(get_abi3_version())?; + if config.abi.kind.is_free_threaded() { + abi_builder = abi_builder.free_threaded()?; + } + config.abi = abi_builder.finalize(); Ok(config) }) @@ -553,10 +508,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - let mut implementation = None; - let mut version = None; + let mut abi = None; let mut shared = None; - let mut stable_abi = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -578,10 +531,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .ok_or_else(|| format!("expected key=value pair on line {}", i + 1))?, ); match key { - "implementation" => parse_value!(implementation, value), - "version" => parse_value!(version, value), + "abi" => parse_value!(abi, value), "shared" => parse_value!(shared, value), - "stable_abi" => parse_value!(stable_abi, value), "lib_name" => parse_value!(lib_name, value), "lib_dir" => parse_value!(lib_dir, value), "executable" => parse_value!(executable, value), @@ -598,16 +549,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } - let version = version.ok_or("missing value for version")?; - let implementation = implementation.unwrap_or(PythonImplementation::CPython); - let stable_abi = stable_abi.unwrap_or(CPythonABI::VersionSpecific); let build_flags = build_flags.unwrap_or_default(); Ok(InterpreterConfig { - implementation, - version, + abi: abi.unwrap(), shared: shared.unwrap_or(true), - stable_abi, lib_name, lib_dir, executable, @@ -627,13 +573,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) #[cfg(any(test, feature = "resolve-config"))] pub(crate) fn apply_default_lib_name_to_config_file(&mut self, target: &Triple) { if self.lib_name.is_none() { - self.lib_name = Some(default_lib_name_for_target( - self.version, - self.implementation, - self.stable_abi, - self.is_free_threaded(), - target, - )); + self.lib_name = Some(default_lib_name_for_target(self.abi, target)); } } @@ -682,10 +622,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - write_line!(implementation)?; - write_line!(version)?; + write_line!(abi)?; write_line!(shared)?; - write_line!(stable_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -731,49 +669,181 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) envs, ) } +} - pub fn is_free_threaded(&self) -> bool { - self.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) - } - - /// Updates configured ABI to build for to the requested abi3 version - /// This is a no-op for platforms where abi3 is not supported - fn fixup_for_abi3_version(&mut self, abi3_version: Option) -> Result<()> { - // PyPy, GraalPy, and the free-threaded build don't support abi3; don't adjust the version - if self.implementation.is_pypy() - || self.implementation.is_graalpy() - || self.is_free_threaded() - { - return Ok(()); - } - - self.fixup_for_stable_abi_version(abi3_version, is_abi3)?; +#[derive(Debug)] +pub struct PythonAbiBuilder { + implementation: PythonImplementation, + version: PythonVersion, + kind: Option, +} - Ok(()) +impl PythonAbiBuilder { + pub fn new(implementation: PythonImplementation, version: PythonVersion) -> PythonAbiBuilder { + PythonAbiBuilder { + implementation, + version, + kind: None, + } } - /// Core logic for pinning a Python stable ABI version to minimum and maximum supported versions - fn fixup_for_stable_abi_version( - &mut self, - abi_version: Option, - abi_check: impl Fn() -> bool, - ) -> Result<()> { - if let Some(version) = abi_version { + pub fn abi3(self, abi3_version: Option) -> Result { + if self.kind.is_some() { + bail!("Target ABI already chosen!") + } + + // PyPy and GraalPy don't support abi3; don't adjust the version + if self.implementation.is_pypy() || self.implementation.is_graalpy() { + return Ok(PythonAbiBuilder { + implementation: self.implementation, + version: self.version, + kind: self.kind, + }); + } + let mut build_version = self.version; + if let Some(version) = abi3_version { ensure!( version <= self.version, "cannot set a minimum Python version {} higher than the interpreter version {} \ - (the minimum Python version is implied by the abi3-py3{} feature)", + (the minimum Python version is implied by the abi3-py3{} feature)", version, self.version, version.minor, ); - self.version = version; - } else if abi_check() && self.version.minor > STABLE_ABI_MAX_MINOR { + build_version = version; + } else if self.version.minor > STABLE_ABI_MAX_MINOR { warn!("Automatically falling back to abi3-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); - self.version.minor = STABLE_ABI_MAX_MINOR; + build_version.minor = STABLE_ABI_MAX_MINOR; } - Ok(()) + Ok(PythonAbiBuilder { + kind: Some(PythonAbiKind::Abi3), + version: build_version, + ..self + }) + } + + pub fn adjust_from_build_env( + self, + abi3_version: Option, + ) -> Result { + if is_abi3() { + self.abi3(abi3_version) + } else { + Ok(PythonAbiBuilder { ..self }) + } + } + + pub fn free_threaded(self) -> Result { + if self.kind.is_some() { + bail!("Target ABI already chosen!") + } + if self.version < PythonVersion::PY313 { + let version = self.version; + bail!( + "Free-threaded builds on Python versions before 3.13, tried to build for {version}" + ) + } + Ok(PythonAbiBuilder { + kind: Some(PythonAbiKind::VersionSpecific(true)), + ..self + }) + } + + pub fn finalize(self) -> PythonAbi { + // default to GIL-enabled version-specific ABI + let kind = self.kind.unwrap_or(PythonAbiKind::VersionSpecific(false)); + PythonAbi { + implementation: self.implementation, + kind, + version: self.version, + } + } +} + +#[non_exhaustive] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct PythonAbi { + /// The Python implementation flavor. + /// + /// Serialized to `implementation`. + pub implementation: PythonImplementation, + + /// The ABI flavor + /// + /// Serialized to `kind` + pub kind: PythonAbiKind, + + /// Python `X.Y` version. e.g. `3.9`. + /// + /// Serialized to `version`. + pub version: PythonVersion, +} + +impl Display for PythonAbi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let implementation = self.implementation; + let kind = self.kind; + let version = self.version; + write!(f, "{implementation}-{kind}-{version}") + } +} + +impl FromStr for PythonAbi { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + let parts: Vec<&str> = value.split("-").collect(); + let implementation = parts[0].parse()?; + let kind = parts[1].parse()?; + let version: PythonVersion = parts[2].parse()?; + Ok(PythonAbi { + implementation, + kind, + version, + }) + } +} + +/// The "kind" of stable ABI. Either abi3 or abi3t currently. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PythonAbiKind { + /// The original stable ABI, supporting Python 3.2 and up + Abi3, + /// Version specific ABI, which may be different on the free-threaded build (true) or gil-enabled build (false) + VersionSpecific(bool), +} + +impl Display for PythonAbiKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PythonAbiKind::Abi3 => write!(f, "abi3"), + PythonAbiKind::VersionSpecific(gil_disabled) => { + write!(f, "version_specific({gil_disabled})") + } + } + } +} + +impl FromStr for PythonAbiKind { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + match value { + "abi3" => Ok(PythonAbiKind::Abi3), + "version_specific(true)" => Ok(PythonAbiKind::VersionSpecific(true)), + "version_specific(false)" => Ok(PythonAbiKind::VersionSpecific(false)), + _ => Err(format!("Unrecognized ABI name: {value}").into()), + } + } +} + +impl PythonAbiKind { + pub fn is_free_threaded(&self) -> bool { + match self { + PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled, + PythonAbiKind::Abi3 => false, + } } } @@ -788,7 +858,7 @@ impl PythonVersion { major: 3, minor: 15, }; - pub const PY313: Self = PythonVersion { + pub(crate) const PY313: Self = PythonVersion { major: 3, minor: 13, }; @@ -796,10 +866,16 @@ impl PythonVersion { major: 3, minor: 12, }; - const PY310: Self = PythonVersion { + pub const PY311: Self = PythonVersion { + major: 3, + minor: 11, + }; + pub const PY310: Self = PythonVersion { major: 3, minor: 10, }; + pub const PY39: Self = PythonVersion { major: 3, minor: 9 }; + pub const PY38: Self = PythonVersion { major: 3, minor: 8 }; } impl Display for PythonVersion { @@ -1618,27 +1694,24 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let implementation = PythonImplementation::CPython; - let stable_abi = CPythonABI::ABI3; + let abi_builder = PythonAbiBuilder::new(implementation, version).abi3(Some(version))?; + let abi = abi_builder.finalize(); let lib_name = if host.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows( - version, - implementation, - stable_abi, - false, - false, - false, - )?) + Some(default_lib_name_windows(abi, false, false)?) } else { None }; Ok(InterpreterConfig { - implementation, - version, + abi, shared: true, - stable_abi, lib_name, lib_dir: None, executable: None, @@ -1728,56 +1793,36 @@ const WINDOWS_STABLE_ABI_DEBUG_LIB_NAME: &str = "python3_d"; /// Generates the default library name for the target platform. #[allow(dead_code)] -fn default_lib_name_for_target( - version: PythonVersion, - implementation: PythonImplementation, - stable_abi: CPythonABI, - gil_disabled: bool, - target: &Triple, -) -> String { +fn default_lib_name_for_target(abi: PythonAbi, target: &Triple) -> String { if target.operating_system == OperatingSystem::Windows { - default_lib_name_windows( - version, - implementation, - stable_abi, - false, - false, - gil_disabled, - ) - .unwrap() + default_lib_name_windows(abi, false, false).unwrap() } else { default_lib_name_unix( - version, - implementation, - stable_abi, + abi, target.operating_system == OperatingSystem::Cygwin, None, - gil_disabled, ) .unwrap() } } -fn default_lib_name_windows( - version: PythonVersion, - implementation: PythonImplementation, - stable_abi: CPythonABI, - mingw: bool, - debug: bool, - gil_disabled: bool, -) -> Result { - if implementation.is_pypy() { +fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result { + if abi.implementation.is_pypy() { // PyPy on Windows ships `libpypy3.X-c.dll` (e.g. `libpypy3.11-c.dll`), // not CPython's `pythonXY.dll`. With raw-dylib linking we need the real // DLL name rather than the import-library alias. - Ok(format!("libpypy{}.{}-c", version.major, version.minor)) - } else if debug && version < PythonVersion::PY310 { + Ok(format!( + "libpypy{}.{}-c", + abi.version.major, abi.version.minor + )) + } else if debug && abi.version < PythonVersion::PY310 { // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 - Ok(format!("python{}{}_d", version.major, version.minor)) - } else if stable_abi != CPythonABI::VersionSpecific - && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) - { + Ok(format!( + "python{}{}_d", + abi.version.major, abi.version.minor + )) + } else if matches!(abi.kind, PythonAbiKind::Abi3) && !abi.implementation.is_graalpy() { if debug { Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) } else { @@ -1785,50 +1830,52 @@ fn default_lib_name_windows( } } else if mingw { ensure!( - !gil_disabled, + !abi.kind.is_free_threaded(), "MinGW free-threaded builds are not currently tested or supported" ); // https://packages.msys2.org/base/mingw-w64-python - Ok(format!("python{}.{}", version.major, version.minor)) - } else if gil_disabled { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) + } else if abi.kind.is_free_threaded() { + ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); if debug { - Ok(format!("python{}{}t_d", version.major, version.minor)) + Ok(format!( + "python{}{}t_d", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}{}t", version.major, version.minor)) + Ok(format!("python{}{}t", abi.version.major, abi.version.minor)) } } else if debug { - Ok(format!("python{}{}_d", version.major, version.minor)) + Ok(format!( + "python{}{}_d", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}{}", version.major, version.minor)) + Ok(format!("python{}{}", abi.version.major, abi.version.minor)) } } -fn default_lib_name_unix( - version: PythonVersion, - implementation: PythonImplementation, - stable_abi: CPythonABI, - cygwin: bool, - ld_version: Option<&str>, - gil_disabled: bool, -) -> Result { - match implementation { +fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) -> Result { + match abi.implementation { PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && stable_abi != CPythonABI::VersionSpecific { + if cygwin && matches!(abi.kind, PythonAbiKind::Abi3) { Ok("python3".to_string()) - } else if gil_disabled { - ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); - Ok(format!("python{}.{}t", version.major, version.minor)) + } else if abi.kind.is_free_threaded() { + ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); + Ok(format!( + "python{}.{}t", + abi.version.major, abi.version.minor + )) } else { - Ok(format!("python{}.{}", version.major, version.minor)) + Ok(format!("python{}.{}", abi.version.major, abi.version.minor)) } } }, PythonImplementation::PyPy => match ld_version { Some(ld_version) => Ok(format!("pypy{ld_version}-c")), - None => Ok(format!("pypy{}.{}-c", version.major, version.minor)), + None => Ok(format!("pypy{}.{}-c", abi.version.major, abi.version.minor)), }, PythonImplementation::GraalPy => Ok("python-native".to_string()), @@ -1951,8 +1998,7 @@ pub fn find_interpreter() -> Result { fn get_host_interpreter(abi3_version: Option) -> Result { let interpreter_path = find_interpreter()?; - let mut interpreter_config = InterpreterConfig::from_interpreter(interpreter_path)?; - interpreter_config.fixup_for_abi3_version(abi3_version)?; + let interpreter_config = InterpreterConfig::from_interpreter(interpreter_path, abi3_version)?; Ok(interpreter_config) } @@ -1964,9 +2010,15 @@ fn get_host_interpreter(abi3_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { - let mut interpreter_config = load_cross_compile_config(cross_config)?; - interpreter_config.fixup_for_abi3_version(get_abi3_version())?; - Some(interpreter_config) + let mut config = load_cross_compile_config(cross_config)?; + let mut abi_builder = PythonAbiBuilder::new(config.abi.implementation, config.abi.version) + .adjust_from_build_env(get_abi3_version())?; + if config.abi.kind.is_free_threaded() { + abi_builder = abi_builder.free_threaded()?; + } + config.abi = abi_builder.finalize(); + + Some(config) } else { None }; @@ -2049,16 +2101,18 @@ mod tests { #[test] fn test_config_file_roundtrip() { + let abi_builder = + PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) + .abi3(None) + .unwrap(); let config = InterpreterConfig { - stable_abi: CPythonABI::ABI3, + abi: abi_builder.finalize(), build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), - implementation: PythonImplementation::CPython, lib_name: Some("lib_name".into()), lib_dir: Some("lib_dir".into()), shared: true, - version: MINIMUM_SUPPORTED_VERSION, suppress_build_script_link_lines: true, extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], python_framework_prefix: None, @@ -2069,9 +2123,9 @@ mod tests { assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); // And some different options, for variety - + let abi_builder = PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY310); let config = InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: abi_builder.finalize(), build_flags: { let mut flags = HashSet::new(); flags.insert(BuildFlag::Py_DEBUG); @@ -2080,14 +2134,9 @@ mod tests { }, pointer_width: None, executable: None, - implementation: PythonImplementation::PyPy, lib_dir: None, lib_name: None, shared: true, - version: PythonVersion { - major: 3, - minor: 10, - }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2101,15 +2150,16 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { let config = InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) + .abi3(None) + .unwrap() + .finalize(), build_flags: BuildFlags::default(), pointer_width: Some(32), executable: Some("executable".into()), - implementation: PythonImplementation::CPython, lib_name: Some("lib_name".into()), lib_dir: Some("lib_dir\\n".into()), shared: true, - version: MINIMUM_SUPPORTED_VERSION, suppress_build_script_link_lines: true, extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], python_framework_prefix: None, @@ -2126,12 +2176,12 @@ mod tests { fn test_config_file_defaults() { // Only version is required assert_eq!( - InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), + InterpreterConfig::from_reader("abi=CPython-version_specific(false)-3.8".as_bytes()) + .unwrap(), InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2148,13 +2198,14 @@ mod tests { fn test_config_file_unknown_keys() { // ext_suffix is unknown to pyo3-build-config, but it shouldn't error assert_eq!( - InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) - .unwrap(), + InterpreterConfig::from_reader( + "abi=CPython-version_specific(false)-3.8\next_suffix=.python38.so".as_bytes() + ) + .unwrap(), InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -2254,15 +2305,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, - implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.8".into()), shared: true, - version: PythonVersion { major: 3, minor: 8 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2284,15 +2334,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, - implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.8".into()), shared: true, - version: PythonVersion { major: 3, minor: 8 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2311,15 +2360,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), pointer_width: Some(64), executable: None, - implementation: PythonImplementation::CPython, lib_dir: Some("/usr/lib".into()), lib_name: Some("python3.8".into()), shared: false, - version: PythonVersion { major: 3, minor: 8 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, @@ -2335,10 +2383,11 @@ mod tests { assert_eq!( default_abi3_config(&host, min_version).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .abi3(None) + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -2359,10 +2408,11 @@ mod tests { assert_eq!( default_abi3_config(&host, min_version).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::ABI3, lib_name: None, lib_dir: None, executable: None, @@ -2394,10 +2444,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python38".into()), lib_dir: Some("C:\\some\\path".into()), executable: None, @@ -2429,10 +2478,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python38".into()), lib_dir: Some("/usr/lib/mingw".into()), executable: None, @@ -2464,10 +2512,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3.9".into()), lib_dir: Some("/usr/arm64/lib".into()), executable: None, @@ -2498,13 +2545,9 @@ mod tests { assert_eq!( default_cross_compile(&cross_config).unwrap(), InterpreterConfig { - implementation: PythonImplementation::PyPy, - version: PythonVersion { - major: 3, - minor: 11 - }, + abi: PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("pypy3.11-c".into()), lib_dir: None, executable: None, @@ -2519,35 +2562,28 @@ mod tests { #[test] fn default_lib_name_windows() { - use CPythonABI::*; - use PythonImplementation::*; assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, false, ) .unwrap(), "python39", ); - assert!(super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, - false, - false, - true, - ) - .is_err()); + // free-threaded Python 3.9 builds should be impossible + assert!( + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .free_threaded() + .is_err() + ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2556,34 +2592,32 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), true, false, - false, ) .unwrap(), "python3.9", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), true, false, - false, ) .unwrap(), "python3", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - PyPy, - ABI3, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2592,13 +2626,10 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - ABI3, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2607,12 +2638,12 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), "python39_d", @@ -2621,129 +2652,83 @@ mod tests { // to workaround https://github.com/python/cpython/issues/101614 assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), "python39_d", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 10 - }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) + .abi3(None) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), "python3_d", ); - // Python versions older than 3.13 don't support gil_disabled - assert!(super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - VersionSpecific, - false, - false, - true, - ) - .is_err()); // mingw and free-threading are incompatible (until someone adds support) assert!(super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), true, false, - true, ) .is_err()); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), false, false, - true, ) .unwrap(), "python313t", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - ABI3, // abi3 true should not affect the free-threaded lib name - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), false, true, ) .unwrap(), - "python313t", - ); - assert_eq!( - super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - VersionSpecific, - false, - true, - true, - ) - .unwrap(), "python313t_d", ); } #[test] fn default_lib_name_unix() { - use CPythonABI::*; - use PythonImplementation::*; // Defaults to pythonX.Y for CPython 3.8+ assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 8 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), false, None, - false ) .unwrap(), "python3.8", ); assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, None, - false ) .unwrap(), "python3.9", @@ -2751,12 +2736,10 @@ mod tests { // Can use ldversion to override for CPython assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, Some("3.8d"), - false ) .unwrap(), "python3.8d", @@ -2765,15 +2748,9 @@ mod tests { // PyPy 3.11 includes ldversion assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311).finalize(), false, None, - false ) .unwrap(), "pypy3.11-c", @@ -2781,12 +2758,9 @@ mod tests { assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - PyPy, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39).finalize(), false, Some("3.11d"), - false ) .unwrap(), "pypy3.11d-c", @@ -2795,44 +2769,25 @@ mod tests { // free-threading adds a t suffix assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - VersionSpecific, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), false, None, - true ) .unwrap(), "python3.13t", ); - // 3.12 and older are incompatible with gil_disabled - assert!(super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 12, - }, - CPython, - VersionSpecific, - false, - None, - true, - ) - .is_err()); // cygwin abi3 links to unversioned libpython assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - ABI3, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .abi3(None) + .unwrap() + .finalize(), true, None, - false ) .unwrap(), "python3", @@ -2892,47 +2847,34 @@ mod tests { #[test] fn interpreter_version_reduced_to_abi3() { - let mut config = InterpreterConfig { - stable_abi: CPythonABI::ABI3, + let config = InterpreterConfig { + abi: PythonAbiBuilder::new( + PythonImplementation::CPython, + // Make this greater than the target abi3 version to reduce to below + PythonVersion { major: 3, minor: 9 }, + ) + .abi3(Some(PythonVersion::PY38)) + .unwrap() + .finalize(), build_flags: BuildFlags::default(), pointer_width: None, executable: None, - implementation: PythonImplementation::CPython, lib_dir: None, lib_name: None, shared: true, - // Make this greater than the target abi3 version to reduce to below - version: PythonVersion { major: 3, minor: 9 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], python_framework_prefix: None, }; - config - .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 8 })) - .unwrap(); - assert_eq!(config.version, PythonVersion { major: 3, minor: 8 }); + assert_eq!(config.abi.version, PythonVersion { major: 3, minor: 8 }); } #[test] fn abi3_version_cannot_be_higher_than_interpreter() { - let mut config = InterpreterConfig { - stable_abi: CPythonABI::ABI3, - build_flags: BuildFlags::new(), - pointer_width: None, - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: None, - lib_name: None, - shared: true, - version: PythonVersion { major: 3, minor: 8 }, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - - assert!(config - .fixup_for_abi3_version(Some(PythonVersion { major: 3, minor: 9 })) + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38); + assert!(builder + .abi3(Some(PythonVersion { major: 3, minor: 9 })) .unwrap_err() .to_string() .contains( @@ -3114,13 +3056,9 @@ mod tests { #[test] fn test_build_script_outputs_base() { let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 11, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY311) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3141,7 +3079,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.abi + }, ..interpreter_config }; assert_eq!( @@ -3159,10 +3100,11 @@ mod tests { #[test] fn test_build_script_outputs_abi3() { let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::ABI3, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3183,7 +3125,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.abi + }, ..interpreter_config }; assert_eq!( @@ -3202,13 +3147,11 @@ mod tests { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3238,10 +3181,9 @@ mod tests { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_DEBUG); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3293,10 +3235,9 @@ mod tests { #[test] fn test_apply_default_lib_name_to_config_file() { let mut config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -3323,8 +3264,8 @@ mod tests { assert_eq!(config.lib_name, Some("python39".into())); // PyPy - config.implementation = PythonImplementation::PyPy; - config.version = PythonVersion { + config.abi.implementation = PythonImplementation::PyPy; + config.abi.version = PythonVersion { major: 3, minor: 11, }; @@ -3336,11 +3277,11 @@ mod tests { config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("libpypy3.11-c".into())); - config.implementation = PythonImplementation::CPython; + config.abi.implementation = PythonImplementation::CPython; // Free-threaded - config.build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - config.version = PythonVersion { + config.abi.kind = PythonAbiKind::VersionSpecific(true); + config.abi.version = PythonVersion { major: 3, minor: 13, }; @@ -3359,7 +3300,10 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.stable_abi = CPythonABI::ABI3; + config.abi = PythonAbi { + kind: PythonAbiKind::Abi3, + ..config.abi + }; config.lib_name = None; config.apply_default_lib_name_to_config_file(&unix); assert_eq!(config.lib_name, Some("python3.13".into())); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index e0a1d5070f5..457e1aa793d 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -20,7 +20,7 @@ use std::{env, process::Command, str::FromStr, sync::OnceLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple, + CrossCompileConfig, InterpreterConfig, PythonAbi, PythonImplementation, PythonVersion, Triple, }; use target_lexicon::OperatingSystem; @@ -311,7 +311,7 @@ pub mod pyo3_build_script_impl { } pub use crate::impl_::{ cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config, - target_triple_from_env, CPythonABI, InterpreterConfig, PythonVersion, + target_triple_from_env, InterpreterConfig, PythonAbi, PythonAbiKind, PythonVersion, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. @@ -390,13 +390,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.implementation { + let implementation = match interpreter_config.abi.implementation { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.version; + let version = &interpreter_config.abi.version; let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ @@ -485,13 +485,9 @@ mod tests { let mut buf = Vec::new(); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, @@ -528,13 +524,9 @@ mod tests { #[cfg(feature = "resolve-config")] fn test_maximum_version_exceeded_formatting() { let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, + abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .finalize(), shared: true, - stable_abi: CPythonABI::VersionSpecific, lib_name: None, lib_dir: None, executable: None, diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 7434f4dbc2b..b749594111e 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -49,7 +49,8 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .strip_suffix(".html") .unwrap(); - if pyo3_build_config::get().version < PythonVersion::PY315 && struct_name == "PyBytesWriter" + if pyo3_build_config::get().abi.version < PythonVersion::PY315 + && struct_name == "PyBytesWriter" { // PyBytesWriter was added in Python 3.15 continue; @@ -170,7 +171,7 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream let field_ident = Ident::new(&field_name, Span::call_site()); - let bindgen_field_ident = if (pyo3_build_config::get().version >= PythonVersion::PY312) + let bindgen_field_ident = if (pyo3_build_config::get().abi.version >= PythonVersion::PY312) && struct_name == "PyObject" && field_name == "ob_refcnt" { diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index eeacbb31d15..65cddd23bcb 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -2,8 +2,8 @@ use pyo3_build_config::{ bail, ensure, print_feature_cfgs, pyo3_build_script_impl::{ cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target, - resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, CPythonABI, - InterpreterConfig, MaximumVersionExceeded, PythonVersion, + resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, + InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, }, warn, PythonImplementation, }; @@ -45,31 +45,32 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.implementation { + match interpreter_config.abi.implementation { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; + let interp_version = interpreter_config.abi.version; ensure!( - interpreter_config.version >= versions.min, + interp_version >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interp_version, versions.min, ); let v_plus_1 = PythonVersion { major: versions.max.major, minor: versions.max.minor + 1, }; - if interpreter_config.version == v_plus_1 { + if interp_version == v_plus_1 { warn!( "Using experimental support for the Python {}.{} ABI. \ Build artifacts may not be compatible with the final release of CPython, \ so do not distribute them.", v_plus_1.major, v_plus_1.minor, ); - } else if interpreter_config.version > v_plus_1 { + } else if interp_version > v_plus_1 { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); - let major = interpreter_config.version.major; - let minor = interpreter_config.version.minor; - if interpreter_config.is_free_threaded() { + let major = interp_version.major; + let minor = interp_version.minor; + if interpreter_config.abi.kind.is_free_threaded() { error.add_help(&format!( "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", )); @@ -83,29 +84,29 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.is_free_threaded() { + if interpreter_config.abi.kind.is_free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, }; ensure!( - interpreter_config.version >= min_free_threaded_version, + interpreter_config.abi.version >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.version, + interpreter_config.abi.version, ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.version >= versions.min, + interpreter_config.abi.version >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.abi.version, versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -113,13 +114,13 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( - interpreter_config.version >= versions.min, + interpreter_config.abi.version >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.abi.version, versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -127,10 +128,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let CPythonABI::ABI3 = interpreter_config.stable_abi { - match interpreter_config.implementation { + if let PythonAbiKind::Abi3 = interpreter_config.abi.kind { + match interpreter_config.abi.implementation { PythonImplementation::CPython => { - if interpreter_config.is_free_threaded() { + if interpreter_config.abi.kind.is_free_threaded() { warn!( "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." ) From 966a82fdd1ba025aeb5c302d90719954b0fde3c9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 15 Apr 2026 16:47:55 -0600 Subject: [PATCH 04/20] fix nox config file --- noxfile.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index b7130b30d4c..bd91e7544b6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1640,8 +1640,7 @@ def set( self._config_file.truncate(0) self._config_file.write( f"""\ -implementation={implementation} -version={version} +abi={implementation}-version_specific(false)-{version} build_flags={",".join(build_flags)} suppress_build_script_link_lines=true """ From 9d9a0e2b7bd3e0634264e920f8d843e2b758d5fd Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 17 Apr 2026 15:22:06 -0600 Subject: [PATCH 05/20] fix ffi-check --- pyo3-ffi-check/macro/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index b749594111e..3c6eb29cfb9 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -153,7 +153,8 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream if struct_name == "PyMemberDef" { // bindgen picked `type_` as the field name to avoid the `type` keyword, but PyO3 uses `type_code` all_fields.remove("type_"); - } else if struct_name == "PyObject" && pyo3_build_config::get().version >= PythonVersion::PY312 + } else if struct_name == "PyObject" + && pyo3_build_config::get().abi.version >= PythonVersion::PY312 { // bindgen picked `__bindgen_anon_1` as the field name for the anonymous union containing ob_refcnt, // PyO3 uses ob_refcnt directly From f52f1d89028af65e0c72d1ad87dc43194c59ab8b Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 14:13:34 -0600 Subject: [PATCH 06/20] Refactor to use the builder pattern for InterpreterConfig --- pyo3-build-config/src/impl_.rs | 1145 ++++++++++++++++++------------- pyo3-build-config/src/lib.rs | 26 +- pyo3-ffi-check/macro/src/lib.rs | 7 +- pyo3-ffi/build.rs | 30 +- 4 files changed, 689 insertions(+), 519 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 32c1b942ada..c1b84a7c12a 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -92,17 +92,39 @@ pub fn target_triple_from_env() -> Triple { /// strategies are used to populate this type. #[cfg_attr(test, derive(Debug, PartialEq, Eq))] pub struct InterpreterConfig { - /// Which abi the build is configured to link against + /// The host Python implementation flavor. /// - /// Serialized to `abi`. - /// See the documentation for the PythonAbi enum for more details. - pub abi: PythonAbi, + /// Serialized to `implementation`. + pub implementation: PythonImplementation, + + /// The host Python `X.Y` version. e.g. `3.9`. + /// + /// Serialized to `version`. + pub version: PythonVersion, /// Whether link library is shared. /// /// Serialized to `shared`. pub shared: bool, + /// The ABI to use for the compilation target. + /// + /// Serialized to `target_abi`. + /// See the documentation for the PythonAbi enum for more details. + pub target_abi: PythonAbi, + + /// Deprecated field used to indicate an abi3 target. + /// + /// Creating an InterpreterConfig struct with `abi3` set to `True`, + /// `interpreter` set to `PythonImplementation::CPython` and `version` set + /// to `PythonVersion {major: 3, minor: 9}` is equivalent to setting `abi` + /// to `PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion + /// {major: 3, minor: 9).abi3().finalize()`. + /// + /// Serialized to `abi3`. + #[deprecated] + pub abi3: bool, + /// The name of the link library defining Python. /// /// This effectively controls the `cargo:rustc-link-lib=` value to @@ -170,24 +192,24 @@ impl InterpreterConfig { #[doc(hidden)] pub fn build_script_outputs(&self) -> Vec { // This should have been checked during pyo3-build-config build time. - assert!(self.abi.version >= MINIMUM_SUPPORTED_VERSION); + assert!(self.target_abi.version >= MINIMUM_SUPPORTED_VERSION); let mut out = vec![]; - for i in MINIMUM_SUPPORTED_VERSION.minor..=self.abi.version.minor { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.target_abi.version.minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } - match self.abi.implementation { + match self.target_abi.implementation { PythonImplementation::CPython => {} PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()), PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), PythonImplementation::RustPython => out.push("cargo:rustc-cfg=RustPython".to_owned()), } - match self.abi.kind { + match self.target_abi.kind { PythonAbiKind::Abi3 => { - if !self.abi.kind.is_free_threaded() { + if !self.target_abi.kind.is_free_threaded() { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } } @@ -317,20 +339,33 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; - let mut abi_builder = - PythonAbiBuilder::new(implementation, version).adjust_from_build_env(abi3_version)?; + let target_version = if let Some(min_version) = abi3_version { + ensure!( + min_version <= version, + "cannot set a minimum Python version {} higher than the interpreter version {} \ + (the minimum Python version is implied by the abi3-py3{} feature)", + min_version, + version, + min_version.minor + ); + min_version + } else { + version + }; + + let mut abi_builder = PythonAbiBuilder::new(implementation, target_version); if gil_disabled { abi_builder = abi_builder.free_threaded()?; } - let abi = abi_builder.finalize(); + let target_abi = abi_builder.finalize(); let cygwin = map["cygwin"].as_str() == "True"; let lib_name = if cfg!(windows) { default_lib_name_windows( - abi, + target_abi, map["mingw"].as_str() == "True", // This is the best heuristic currently available to detect debug build // on Windows from sysconfig - e.g. ext_suffix may be @@ -338,7 +373,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) map["ext_suffix"].starts_with("_d."), )? } else { - default_lib_name_unix(abi, cygwin, map.get("ld_version").map(String::as_str))? + default_lib_name_unix( + target_abi, + cygwin, + map.get("ld_version").map(String::as_str), + )? }; let lib_dir = if cfg!(windows) { @@ -357,18 +396,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .parse() .context("failed to parse calcsize_pointer")?; - Ok(InterpreterConfig { - shared, - abi, - lib_name: Some(lib_name), - lib_dir, - executable: map.get("executable").cloned(), - pointer_width: Some(calcsize_pointer * 8), - build_flags: BuildFlags::from_interpreter(interpreter)?, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix, - }) + Ok(InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi)? + .shared(shared) + .lib_name(Some(lib_name)) + .lib_dir(lib_dir) + .executable(map.get("executable").cloned()) + .pointer_width(calcsize_pointer * 8) + .build_flags(BuildFlags::from_interpreter(interpreter)?)? + .python_framework_prefix(python_framework_prefix) + .finalize()) } /// Generate from parsed sysconfigdata file @@ -414,34 +451,29 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = - PythonAbiBuilder::new(implementation, version).adjust_from_build_env(None)?; + let mut abi_builder = PythonAbiBuilder::from_build_env(implementation, version)?; if gil_disabled { abi_builder = abi_builder.free_threaded()?; } - let abi = abi_builder.finalize(); + let target_abi = abi_builder.finalize(); let lib_name = Some(default_lib_name_unix( - abi, + target_abi, cygwin, sysconfigdata.get_value("LDVERSION"), )?); - let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") - .map(|bytes_width: u32| bytes_width * 8) - .ok(); + let pointer_width = + parse_key!(sysconfigdata, "SIZEOF_VOID_P").map(|bytes_width: u32| bytes_width * 8)?; let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); - Ok(InterpreterConfig { - abi, - shared: shared || framework, - lib_dir, - lib_name, - executable: None, - pointer_width, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix, - }) + Ok(InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi)? + .shared(shared || framework) + .lib_dir(lib_dir) + .lib_name(lib_name) + .pointer_width(pointer_width) + .build_flags(build_flags)? + .python_framework_prefix(python_framework_prefix) + .finalize()) } /// Import an externally-provided config file. @@ -463,13 +495,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse contents of PYO3_CONFIG_FILE")?; // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 // feature. - let mut abi_builder = - PythonAbiBuilder::new(config.abi.implementation, config.abi.version) - .adjust_from_build_env(get_abi3_version())?; - if config.abi.kind.is_free_threaded() { + let mut abi_builder = PythonAbiBuilder::from_build_env( + config.target_abi.implementation, + get_abi3_version().unwrap_or(config.target_abi.version), + )?; + if config.target_abi.kind.is_free_threaded() { abi_builder = abi_builder.free_threaded()?; } - config.abi = abi_builder.finalize(); + config.target_abi = abi_builder.finalize(); Ok(config) }) @@ -508,8 +541,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - let mut abi = None; + let mut implementation = None; + let mut version: Option = None; let mut shared = None; + let mut target_abi = None; + // deprecated in the struct but we still allow it to support old config files + let mut abi3: Option = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -531,8 +568,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .ok_or_else(|| format!("expected key=value pair on line {}", i + 1))?, ); match key { - "abi" => parse_value!(abi, value), + "implementation" => parse_value!(implementation, value), + "version" => parse_value!(version, value), "shared" => parse_value!(shared, value), + "target_abi" => parse_value!(target_abi, value), + "abi3" => parse_value!(abi3, value), "lib_name" => parse_value!(lib_name, value), "lib_dir" => parse_value!(lib_dir, value), "executable" => parse_value!(executable, value), @@ -549,20 +589,44 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } + let version = version.ok_or("missing value for version")?; + let implementation = implementation.unwrap_or(PythonImplementation::CPython); + + let target_abi = if !(target_abi.is_some() || abi3.is_some()) { + PythonAbiBuilder::new(implementation, version).finalize() + } else if abi3.is_some() && abi3.unwrap() { + // should we produce a user-visible warning about this? + PythonAbiBuilder::new(implementation, version) + .abi3() + .unwrap() + .finalize() + } else { + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap() + }; + let build_flags = build_flags.unwrap_or_default(); + let builder = InterpreterConfigBuilder::new(implementation, version) + .target_abi(target_abi)? + .shared(shared.unwrap_or(true)) + .lib_name(lib_name) + .lib_dir(lib_dir) + .executable(executable) + .build_flags(build_flags)? + .suppress_build_script_link_lines(suppress_build_script_link_lines) + .extra_build_script_lines(extra_build_script_lines) + .python_framework_prefix(python_framework_prefix); + + let builder = if let Some(pointer_width) = pointer_width { + builder.pointer_width(pointer_width) + } else { + builder + }; - Ok(InterpreterConfig { - abi: abi.unwrap(), - shared: shared.unwrap_or(true), - lib_name, - lib_dir, - executable, - pointer_width, - build_flags, - suppress_build_script_link_lines: suppress_build_script_link_lines.unwrap_or(false), - extra_build_script_lines, - python_framework_prefix, - }) + Ok(builder.finalize()) } /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. @@ -573,7 +637,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) #[cfg(any(test, feature = "resolve-config"))] pub(crate) fn apply_default_lib_name_to_config_file(&mut self, target: &Triple) { if self.lib_name.is_none() { - self.lib_name = Some(default_lib_name_for_target(self.abi, target)); + self.lib_name = Some(default_lib_name_for_target(self.target_abi, target)); } } @@ -622,8 +686,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - write_line!(abi)?; + write_line!(implementation)?; + write_line!(version)?; write_line!(shared)?; + write_line!(target_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -671,6 +737,181 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } +#[cfg_attr(test, derive(Debug))] +pub struct InterpreterConfigBuilder { + implementation: PythonImplementation, + version: PythonVersion, + shared: Option, + target_abi: Option, + lib_name: Option, + lib_dir: Option, + executable: Option, + pointer_width: Option, + build_flags: Option, + suppress_build_script_link_lines: Option, + extra_build_script_lines: Option>, + python_framework_prefix: Option, +} + +impl InterpreterConfigBuilder { + pub fn new( + implementation: PythonImplementation, + version: PythonVersion, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + implementation, + version, + shared: None, + target_abi: None, + lib_name: None, + lib_dir: None, + executable: None, + pointer_width: None, + build_flags: None, + suppress_build_script_link_lines: None, + extra_build_script_lines: None, + python_framework_prefix: None, + } + } + + pub fn target_abi(self, target_abi: PythonAbi) -> Result { + ensure!(self.target_abi.is_none(), "Target ABI already set!"); + Ok(InterpreterConfigBuilder { + target_abi: Some(target_abi), + ..self + }) + } + + pub fn abi3(self) -> Result { + let implementation = self.implementation; + let version = self.version; + self.target_abi( + PythonAbiBuilder::new(implementation, version) + .abi3() + // this can't panic because abi3() is caleld on a builder with no chosen ABI + .unwrap() + .finalize(), + ) + } + + pub fn free_threaded(self) -> Result { + let implementation: PythonImplementation = self.implementation; + let version: PythonVersion = self.version; + self.target_abi( + PythonAbiBuilder::new(implementation, version) + .free_threaded() + // this can't panic because abi3() is called on a builder with no chosen ABI + .unwrap() + .finalize(), + )? + .build_flags(BuildFlags::default()) + } + + pub fn lib_name(self, lib_name: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { lib_name, ..self } + } + + pub fn pointer_width(self, pointer_width: u32) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + pointer_width: Some(pointer_width), + ..self + } + } + + pub fn executable(self, executable: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { executable, ..self } + } + + pub fn suppress_build_script_link_lines( + self, + suppress_build_script_link_lines: Option, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + suppress_build_script_link_lines, + ..self + } + } + + pub fn extra_build_script_lines( + self, + extra_build_script_lines: Vec, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + extra_build_script_lines: Some(extra_build_script_lines), + ..self + } + } + + pub fn lib_dir(self, lib_dir: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { lib_dir, ..self } + } + + pub fn shared(self, shared: bool) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + shared: Some(shared), + ..self + } + } + + pub fn build_flags(self, build_flags: BuildFlags) -> Result { + ensure!(self.build_flags.is_none(), "Build flags already set!"); + let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + ensure!(self.target_abi.is_some(), "Must target a free-threaded ABI if build flags contain Py_GIL_DISABLED but no target_abi is set"); + ensure!( + self.target_abi.unwrap().kind + == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded), + "build_flags contains Py_GIL_DISABLED but target ABI is not free-threaded" + ); + build_flags + } else if let Some(target_abi) = self.target_abi { + let mut flags = build_flags.clone(); + if target_abi.kind.is_free_threaded() { + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } + flags + } else { + build_flags + }; + Ok(InterpreterConfigBuilder { + build_flags: Some(build_flags), + ..self + }) + } + + pub fn python_framework_prefix( + self, + python_framework_prefix: Option, + ) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + python_framework_prefix, + ..self + } + } + + pub fn finalize(self) -> InterpreterConfig { + #[allow(deprecated)] + InterpreterConfig { + implementation: self.implementation, + version: self.version, + shared: self.shared.unwrap_or(true), + target_abi: self + .target_abi + .unwrap_or(PythonAbiBuilder::new(self.implementation, self.version).finalize()), + abi3: false, + lib_name: self.lib_name, + lib_dir: self.lib_dir, + executable: self.executable, + pointer_width: self.pointer_width, + build_flags: self.build_flags.unwrap_or_default(), + suppress_build_script_link_lines: self + .suppress_build_script_link_lines + .unwrap_or(false), + extra_build_script_lines: self.extra_build_script_lines.unwrap_or(vec![]), + python_framework_prefix: self.python_framework_prefix, + } + } +} + #[derive(Debug)] pub struct PythonAbiBuilder { implementation: PythonImplementation, @@ -687,7 +928,23 @@ impl PythonAbiBuilder { } } - pub fn abi3(self, abi3_version: Option) -> Result { + pub fn from_build_env( + implementation: PythonImplementation, + version: PythonVersion, + ) -> Result { + let builder = PythonAbiBuilder { + implementation, + version, + kind: None, + }; + if is_abi3() { + builder.abi3() + } else { + Ok(builder) + } + } + + pub fn abi3(self) -> Result { if self.kind.is_some() { bail!("Target ABI already chosen!") } @@ -701,17 +958,7 @@ impl PythonAbiBuilder { }); } let mut build_version = self.version; - if let Some(version) = abi3_version { - ensure!( - version <= self.version, - "cannot set a minimum Python version {} higher than the interpreter version {} \ - (the minimum Python version is implied by the abi3-py3{} feature)", - version, - self.version, - version.minor, - ); - build_version = version; - } else if self.version.minor > STABLE_ABI_MAX_MINOR { + if self.version.minor > STABLE_ABI_MAX_MINOR { warn!("Automatically falling back to abi3-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); build_version.minor = STABLE_ABI_MAX_MINOR; } @@ -723,17 +970,6 @@ impl PythonAbiBuilder { }) } - pub fn adjust_from_build_env( - self, - abi3_version: Option, - ) -> Result { - if is_abi3() { - self.abi3(abi3_version) - } else { - Ok(PythonAbiBuilder { ..self }) - } - } - pub fn free_threaded(self) -> Result { if self.kind.is_some() { bail!("Target ABI already chosen!") @@ -741,18 +977,20 @@ impl PythonAbiBuilder { if self.version < PythonVersion::PY313 { let version = self.version; bail!( - "Free-threaded builds on Python versions before 3.13, tried to build for {version}" + "Cannot target free-threaded builds for Python versions before 3.13, tried to build for {version}" ) } Ok(PythonAbiBuilder { - kind: Some(PythonAbiKind::VersionSpecific(true)), + kind: Some(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)), ..self }) } pub fn finalize(self) -> PythonAbi { // default to GIL-enabled version-specific ABI - let kind = self.kind.unwrap_or(PythonAbiKind::VersionSpecific(false)); + let kind = self + .kind + .unwrap_or(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)); PythonAbi { implementation: self.implementation, kind, @@ -810,8 +1048,8 @@ impl FromStr for PythonAbi { pub enum PythonAbiKind { /// The original stable ABI, supporting Python 3.2 and up Abi3, - /// Version specific ABI, which may be different on the free-threaded build (true) or gil-enabled build (false) - VersionSpecific(bool), + /// Version specific ABI, which is different on the free-threaded build + VersionSpecific(GilUsed), } impl Display for PythonAbiKind { @@ -831,8 +1069,12 @@ impl FromStr for PythonAbiKind { fn from_str(value: &str) -> Result { match value { "abi3" => Ok(PythonAbiKind::Abi3), - "version_specific(true)" => Ok(PythonAbiKind::VersionSpecific(true)), - "version_specific(false)" => Ok(PythonAbiKind::VersionSpecific(false)), + "version_specific(free_threaded)" => { + Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) + } + "version_specific(gil_enabled)" => { + Ok(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)) + } _ => Err(format!("Unrecognized ABI name: {value}").into()), } } @@ -841,10 +1083,44 @@ impl FromStr for PythonAbiKind { impl PythonAbiKind { pub fn is_free_threaded(&self) -> bool { match self { - PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled, + PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled == GilUsed::FreeThreaded, PythonAbiKind::Abi3 => false, } } + + pub fn is_abi3(&self) -> bool { + matches!(self, PythonAbiKind::Abi3) + } +} + +/// Whether the ABI is for the GIL-enabled or free-threaded build. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GilUsed { + /// The original PyObject layout + GilEnabled, + /// The free-threaded PyObject layout + FreeThreaded, +} + +impl Display for GilUsed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GilUsed::GilEnabled => write!(f, "gil_enabled"), + GilUsed::FreeThreaded => write!(f, "free_threaded"), + } + } +} + +impl FromStr for GilUsed { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + match value { + "gil_enabled" => Ok(GilUsed::GilEnabled), + "free_threaded" => Ok(GilUsed::FreeThreaded), + _ => Err(format!("Unrecognized ABI name: {value}").into()), + } + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -858,7 +1134,11 @@ impl PythonVersion { major: 3, minor: 15, }; - pub(crate) const PY313: Self = PythonVersion { + pub const PY314: Self = PythonVersion { + major: 3, + minor: 14, + }; + pub const PY313: Self = PythonVersion { major: 3, minor: 13, }; @@ -1698,29 +1978,21 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. - let implementation = PythonImplementation::CPython; - let abi_builder = PythonAbiBuilder::new(implementation, version).abi3(Some(version))?; - let abi = abi_builder.finalize(); - - let lib_name = if host.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows(abi, false, false)?) + let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) + .abi3() + .unwrap(); + // abi3() sets the target_abi on the builder struct so unwrapping is safe + let target_abi = builder.target_abi.unwrap(); + Ok(if host.operating_system == OperatingSystem::Windows { + builder.lib_name(Some(default_lib_name_windows(target_abi, false, false)?)) } else { - None - }; - - Ok(InterpreterConfig { - abi, - shared: true, - lib_name, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }) + builder + } + .finalize()) } /// Detects the cross compilation target interpreter configuration from all @@ -2011,12 +2272,14 @@ fn get_host_interpreter(abi3_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut config = load_cross_compile_config(cross_config)?; - let mut abi_builder = PythonAbiBuilder::new(config.abi.implementation, config.abi.version) - .adjust_from_build_env(get_abi3_version())?; - if config.abi.kind.is_free_threaded() { + let mut abi_builder = PythonAbiBuilder::from_build_env( + config.target_abi.implementation, + get_abi3_version().unwrap_or(config.target_abi.version), + )?; + if config.target_abi.kind.is_free_threaded() { abi_builder = abi_builder.free_threaded()?; } - config.abi = abi_builder.finalize(); + config.target_abi = abi_builder.finalize(); Some(config) } else { @@ -2101,46 +2364,36 @@ mod tests { #[test] fn test_config_file_roundtrip() { - let abi_builder = - PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) - .abi3(None) - .unwrap(); - let config = InterpreterConfig { - abi: abi_builder.finalize(), - build_flags: BuildFlags::default(), - pointer_width: Some(32), - executable: Some("executable".into()), - lib_name: Some("lib_name".into()), - lib_dir: Some("lib_dir".into()), - shared: true, - suppress_build_script_link_lines: true, - extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .pointer_width(32) + .executable(Some("executable".into())) + .lib_dir(Some("lib_name".into())) + .lib_name(Some("lib_name".into())) + .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) + .finalize(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); assert_eq!(config, InterpreterConfig::from_reader(&*buf).unwrap()); // And some different options, for variety - let abi_builder = PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY310); - let config = InterpreterConfig { - abi: abi_builder.finalize(), - build_flags: { - let mut flags = HashSet::new(); - flags.insert(BuildFlag::Py_DEBUG); - flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG"))); - BuildFlags(flags) - }, - pointer_width: None, - executable: None, - lib_dir: None, - lib_name: None, - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, + let version = PythonVersion::PY310; + let implementation = PythonImplementation::PyPy; + let build_flags = { + let mut flags = HashSet::new(); + flags.insert(BuildFlag::Py_DEBUG); + flags.insert(BuildFlag::Other(String::from("Py_SOME_FLAG"))); + BuildFlags(flags) }; + let config = InterpreterConfigBuilder::new(implementation, version) + .build_flags(build_flags) + .unwrap() + .finalize(); + let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2149,21 +2402,17 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { - let config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, MINIMUM_SUPPORTED_VERSION) - .abi3(None) - .unwrap() - .finalize(), - build_flags: BuildFlags::default(), - pointer_width: Some(32), - executable: Some("executable".into()), - lib_name: Some("lib_name".into()), - lib_dir: Some("lib_dir\\n".into()), - shared: true, - suppress_build_script_link_lines: true, - extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .pointer_width(32) + .executable(Some("executable".into())) + .lib_name(Some("lib_name".into())) + .lib_dir(Some("lib_dir\\n".into())) + .extra_build_script_lines(vec!["cargo:test1".to_string(), "cargo:test2".to_string()]) + .finalize(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2175,46 +2424,23 @@ mod tests { #[test] fn test_config_file_defaults() { // Only version is required + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( - InterpreterConfig::from_reader("abi=CPython-version_specific(false)-3.8".as_bytes()) - .unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfig::from_reader("version=3.8".as_bytes()).unwrap(), + InterpreterConfigBuilder::new(implementation, version,).finalize() ) } #[test] fn test_config_file_unknown_keys() { // ext_suffix is unknown to pyo3-build-config, but it shouldn't error + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( - InterpreterConfig::from_reader( - "abi=CPython-version_specific(false)-3.8\next_suffix=.python38.so".as_bytes() - ) - .unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfig::from_reader("version=3.8\next_suffix=.python38.so".as_bytes()) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version,).finalize() ) } @@ -2302,21 +2528,17 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .unwrap() + .lib_dir(Some("/usr/lib".into())) + .lib_name(Some("python3.8".into())) + .pointer_width(64) + .finalize() ); } @@ -2331,21 +2553,17 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .unwrap() + .lib_dir(Some("/usr/lib".into())) + .lib_name(Some("python3.8".into())) + .pointer_width(64) + .finalize() ); sysconfigdata = Sysconfigdata::new(); @@ -2357,21 +2575,18 @@ mod tests { sysconfigdata.insert("LIBDIR", "/usr/lib"); sysconfigdata.insert("LDVERSION", "3.8"); sysconfigdata.insert("SIZEOF_VOID_P", "8"); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - build_flags: BuildFlags::from_sysconfigdata(&sysconfigdata), - pointer_width: Some(64), - executable: None, - lib_dir: Some("/usr/lib".into()), - lib_name: Some("python3.8".into()), - shared: false, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new(implementation, version,) + .build_flags(BuildFlags::from_sysconfigdata(&sysconfigdata)) + .unwrap() + .lib_dir(Some("/usr/lib".into())) + .lib_name(Some("python3.8".into())) + .pointer_width(64) + .shared(false) + .finalize() ); } @@ -2380,49 +2595,27 @@ mod tests { let host = triple!("x86_64-pc-windows-msvc"); let min_version = "3.8".parse().unwrap(); - assert_eq!( - default_abi3_config(&host, min_version).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .abi3(None) - .unwrap() - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .lib_name(Some("python3".into())) + .finalize(); + assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } #[test] fn unix_hardcoded_abi3_compile() { let host = triple!("x86_64-unknown-linux-gnu"); let min_version = "3.9".parse().unwrap(); - - assert_eq!( - default_abi3_config(&host, min_version).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) - .unwrap() - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .finalize(); + assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } #[test] @@ -2441,22 +2634,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: Some("python38".into()), - lib_dir: Some("C:\\some\\path".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python38".into())) + .lib_dir(Some("C:\\some\\path".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2475,22 +2659,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: Some("python38".into()), - lib_dir: Some("/usr/lib/mingw".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python38".into())) + .lib_dir(Some("/usr/lib/mingw".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2509,22 +2684,13 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), - shared: true, - lib_name: Some("python3.9".into()), - lib_dir: Some("/usr/arm64/lib".into()), - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python3.9".into())) + .lib_dir(Some("/usr/arm64/lib".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2542,22 +2708,12 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) - .finalize(), - shared: true, - lib_name: Some("pypy3.11-c".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } - ); + let implementation = PythonImplementation::PyPy; + let version = PythonVersion::PY311; + let config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("pypy3.11-c".into())) + .finalize(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2581,7 +2737,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2603,7 +2759,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), true, @@ -2615,7 +2771,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2627,7 +2783,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2639,7 +2795,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2653,7 +2809,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2665,7 +2821,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) - .abi3(None) + .abi3() .unwrap() .finalize(), false, @@ -2783,7 +2939,7 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .abi3(None) + .abi3() .unwrap() .finalize(), true, @@ -2794,6 +2950,25 @@ mod tests { ); } + #[test] + fn abi_builder_error_paths() { + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) + .free_threaded() + .unwrap() + .abi3(); + assert!(builder.is_err()); + assert!(builder + .unwrap_err() + .to_string() + .contains("ABI already chosen!")); + + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .free_threaded(); + + assert!(builder.is_err()); + assert!(builder.unwrap_err().to_string().contains("Cannot target")); + } + #[test] fn parse_cross_python_version() { let env_vars = CrossCompileEnvVars { @@ -2846,40 +3021,36 @@ mod tests { } #[test] - fn interpreter_version_reduced_to_abi3() { - let config = InterpreterConfig { - abi: PythonAbiBuilder::new( - PythonImplementation::CPython, - // Make this greater than the target abi3 version to reduce to below - PythonVersion { major: 3, minor: 9 }, + fn target_abi3_version_different_from_host() { + let implementation = PythonImplementation::CPython; + let host_version = PythonVersion::PY39; + let target_version = PythonVersion::PY38; + let config = InterpreterConfigBuilder::new(implementation, host_version) + .target_abi( + PythonAbiBuilder::new(implementation, target_version) + .abi3() + .unwrap() + .finalize(), ) - .abi3(Some(PythonVersion::PY38)) .unwrap() - .finalize(), - build_flags: BuildFlags::default(), - pointer_width: None, - executable: None, - lib_dir: None, - lib_name: None, - shared: true, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - - assert_eq!(config.abi.version, PythonVersion { major: 3, minor: 8 }); + .finalize(); + assert_eq!(config.target_abi.version, target_version); + assert_eq!(config.version, host_version); } #[test] fn abi3_version_cannot_be_higher_than_interpreter() { - let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38); - assert!(builder - .abi3(Some(PythonVersion { major: 3, minor: 9 })) - .unwrap_err() - .to_string() - .contains( - "cannot set a minimum Python version 3.9 higher than the interpreter version 3.8" - )); + if !have_python_interpreter() { + return; + } + + let interpreter = get_host_interpreter(Some(PythonVersion { + major: 3, + minor: 45, + })); + assert!(interpreter.unwrap_err().to_string().contains( + "cannot set a minimum Python version 3.45 higher than the interpreter version" + )); } #[test] @@ -2920,23 +3091,21 @@ mod tests { }; let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); + let implementation = PythonImplementation::CPython; assert_eq!( parsed_config, - InterpreterConfig { - stable_abi: CPythonABI::VersionSpecific, - build_flags: BuildFlags(interpreter_config.build_flags.0.clone()), - pointer_width: Some(64), - executable: None, - implementation: PythonImplementation::CPython, - lib_dir: interpreter_config.lib_dir.to_owned(), - lib_name: interpreter_config.lib_name.to_owned(), - shared: true, - version: interpreter_config.version, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - } + InterpreterConfigBuilder::new( + implementation, + interpreter_config.version, + PythonAbiBuilder::new(implementation, interpreter_config.version).finalize() + ) + .build_flags(interpreter_config.build_flags.0.clone()) + .pointer_width(64) + .lib_dir(interpreter_config.lib_dir.to_owned()) + .lib_name(interpreter_config.lib_name.to_owned()) + .finalize() + .unwrap() ) } @@ -3055,19 +3224,11 @@ mod tests { #[test] fn test_build_script_outputs_base() { - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY311) - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY311; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python3".into())) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3079,9 +3240,9 @@ mod tests { ); let interpreter_config = InterpreterConfig { - abi: PythonAbi { + target_abi: PythonAbi { implementation: PythonImplementation::PyPy, - ..interpreter_config.abi + ..interpreter_config.target_abi }, ..interpreter_config }; @@ -3099,21 +3260,13 @@ mod tests { #[test] fn test_build_script_outputs_abi3() { - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3(None) - .unwrap() - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .abi3() + .unwrap() + .lib_name(Some("python3".into())) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3125,9 +3278,9 @@ mod tests { ); let interpreter_config = InterpreterConfig { - abi: PythonAbi { + target_abi: PythonAbi { implementation: PythonImplementation::PyPy, - ..interpreter_config.abi + ..interpreter_config.target_abi }, ..interpreter_config }; @@ -3144,24 +3297,13 @@ mod tests { #[test] fn test_build_script_outputs_gil_disabled() { - let mut build_flags = BuildFlags::default(); - build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .free_threaded() - .unwrap() - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .lib_name(Some("python3".into())) + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3176,24 +3318,51 @@ mod tests { ); } + #[test] + fn test_interpreter_config_builder_gil_disabled_flag() { + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(builder + .build_flags(flags) + .unwrap_err() + .to_string() + .contains("Must target a free-threaded ABI")); + + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(builder + .abi3() + .unwrap() + .build_flags(flags) + .unwrap_err() + .to_string() + .contains("target ABI is not free-threaded")); + + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let config = builder.free_threaded().unwrap(); + assert!(config + .build_flags + .unwrap() + .0 + .contains(&BuildFlag::Py_GIL_DISABLED)) + } + #[test] fn test_build_script_outputs_debug() { let mut build_flags = BuildFlags::default(); build_flags.0.insert(BuildFlag::Py_DEBUG); - let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY38) - .finalize(), - shared: true, - lib_name: Some("python3".into()), - lib_dir: None, - executable: None, - pointer_width: None, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY38; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .lib_name(Some("python3".into())) + .build_flags(build_flags) + .unwrap() + .finalize(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3234,19 +3403,9 @@ mod tests { #[test] fn test_apply_default_lib_name_to_config_file() { - let mut config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .finalize(), - shared: true, - lib_name: None, - lib_dir: None, - executable: None, - pointer_width: None, - build_flags: BuildFlags::default(), - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix: None, - }; + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY39; + let mut config = InterpreterConfigBuilder::new(implementation, version).finalize(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); @@ -3264,8 +3423,8 @@ mod tests { assert_eq!(config.lib_name, Some("python39".into())); // PyPy - config.abi.implementation = PythonImplementation::PyPy; - config.abi.version = PythonVersion { + config.target_abi.implementation = PythonImplementation::PyPy; + config.target_abi.version = PythonVersion { major: 3, minor: 11, }; @@ -3277,11 +3436,11 @@ mod tests { config.apply_default_lib_name_to_config_file(&win_x64); assert_eq!(config.lib_name, Some("libpypy3.11-c".into())); - config.abi.implementation = PythonImplementation::CPython; + config.target_abi.implementation = PythonImplementation::CPython; // Free-threaded - config.abi.kind = PythonAbiKind::VersionSpecific(true); - config.abi.version = PythonVersion { + config.target_abi.kind = PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded); + config.target_abi.version = PythonVersion { major: 3, minor: 13, }; @@ -3300,9 +3459,9 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.abi = PythonAbi { + config.target_abi = PythonAbi { kind: PythonAbiKind::Abi3, - ..config.abi + ..config.target_abi }; config.lib_name = None; config.apply_default_lib_name_to_config_file(&unix); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 457e1aa793d..a41eb442b56 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -20,7 +20,8 @@ use std::{env, process::Command, str::FromStr, sync::OnceLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, - CrossCompileConfig, InterpreterConfig, PythonAbi, PythonImplementation, PythonVersion, Triple, + CrossCompileConfig, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, + PythonImplementation, PythonVersion, Triple, }; use target_lexicon::OperatingSystem; @@ -390,13 +391,13 @@ pub mod pyo3_build_script_impl { interpreter_config: &InterpreterConfig, supported_version: PythonVersion, ) -> Self { - let implementation = match interpreter_config.abi.implementation { + let implementation = match interpreter_config.target_abi.implementation { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.abi.version; + let version = &interpreter_config.target_abi.version; let message = format!( "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ = help: this package is being built with PyO3 version {current_version}\n\ @@ -483,10 +484,14 @@ mod tests { #[test] fn python_framework_link_args() { let mut buf = Vec::new(); - + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let target_abi = PythonAbiBuilder::new(implementation, version).finalize(); let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .finalize(), + implementation, + version, + target_abi, + abi3: false, shared: true, lib_name: None, lib_dir: None, @@ -523,9 +528,14 @@ mod tests { #[test] #[cfg(feature = "resolve-config")] fn test_maximum_version_exceeded_formatting() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let target_abi = PythonAbiBuilder::new(implementation, version).finalize(); let interpreter_config = InterpreterConfig { - abi: PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .finalize(), + implementation, + version, + target_abi, + abi3: false, shared: true, lib_name: None, lib_dir: None, diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 3c6eb29cfb9..82a90bfcfd3 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -49,7 +49,7 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .strip_suffix(".html") .unwrap(); - if pyo3_build_config::get().abi.version < PythonVersion::PY315 + if pyo3_build_config::get().target_abi.version < PythonVersion::PY315 && struct_name == "PyBytesWriter" { // PyBytesWriter was added in Python 3.15 @@ -154,7 +154,7 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream // bindgen picked `type_` as the field name to avoid the `type` keyword, but PyO3 uses `type_code` all_fields.remove("type_"); } else if struct_name == "PyObject" - && pyo3_build_config::get().abi.version >= PythonVersion::PY312 + && pyo3_build_config::get().target_abi.version >= PythonVersion::PY312 { // bindgen picked `__bindgen_anon_1` as the field name for the anonymous union containing ob_refcnt, // PyO3 uses ob_refcnt directly @@ -172,7 +172,8 @@ pub fn for_all_fields(input: proc_macro::TokenStream) -> proc_macro::TokenStream let field_ident = Ident::new(&field_name, Span::call_site()); - let bindgen_field_ident = if (pyo3_build_config::get().abi.version >= PythonVersion::PY312) + let bindgen_field_ident = if (pyo3_build_config::get().target_abi.version + >= PythonVersion::PY312) && struct_name == "PyObject" && field_name == "ob_refcnt" { diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 65cddd23bcb..34300893461 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -45,10 +45,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.abi.implementation { + match interpreter_config.target_abi.implementation { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; - let interp_version = interpreter_config.abi.version; + let interp_version = interpreter_config.target_abi.version; ensure!( interp_version >= versions.min, "the configured Python interpreter version ({}) is lower than PyO3's minimum supported version ({})", @@ -70,7 +70,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); let major = interp_version.major; let minor = interp_version.minor; - if interpreter_config.abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { error.add_help(&format!( "the free-threaded build of CPython {major}{minor} does not support the limited API so this check cannot be suppressed.", )); @@ -84,29 +84,29 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } - if interpreter_config.abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { let min_free_threaded_version = PythonVersion { major: 3, minor: 14, }; ensure!( - interpreter_config.abi.version >= min_free_threaded_version, + interpreter_config.target_abi.version >= min_free_threaded_version, "PyO3 does not support the free-threaded build of CPython versions below {}, the selected Python version is {}", min_free_threaded_version, - interpreter_config.abi.version, + interpreter_config.target_abi.version, ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.abi.version >= versions.min, + interpreter_config.target_abi.version >= versions.min, "the configured PyPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.abi.version, + interpreter_config.target_abi.version, versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.abi.version > versions.max { + if interpreter_config.target_abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -114,13 +114,13 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; ensure!( - interpreter_config.abi.version >= versions.min, + interpreter_config.target_abi.version >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.abi.version, + interpreter_config.target_abi.version, versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.abi.version > versions.max { + if interpreter_config.target_abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -128,10 +128,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let PythonAbiKind::Abi3 = interpreter_config.abi.kind { - match interpreter_config.abi.implementation { + if let PythonAbiKind::Abi3 = interpreter_config.target_abi.kind { + match interpreter_config.target_abi.implementation { PythonImplementation::CPython => { - if interpreter_config.abi.kind.is_free_threaded() { + if interpreter_config.target_abi.kind.is_free_threaded() { warn!( "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." ) From 77039d27c2a3bf3198526008828280631ed23a57 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 14:17:42 -0600 Subject: [PATCH 07/20] revert noxfile change --- noxfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index bd91e7544b6..b7130b30d4c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1640,7 +1640,8 @@ def set( self._config_file.truncate(0) self._config_file.write( f"""\ -abi={implementation}-version_specific(false)-{version} +implementation={implementation} +version={version} build_flags={",".join(build_flags)} suppress_build_script_link_lines=true """ From 0765953e0c4c98090fa61072daa6290bacaecc85 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 15:27:22 -0600 Subject: [PATCH 08/20] fix clippy-all --- pyo3-build-config/src/impl_.rs | 51 +++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index c1b84a7c12a..8f06734b9e3 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -452,7 +452,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let cygwin = soabi.ends_with("cygwin"); let mut abi_builder = PythonAbiBuilder::from_build_env(implementation, version)?; - if gil_disabled { + if gil_disabled && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } let target_abi = abi_builder.finalize(); @@ -499,7 +499,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) config.target_abi.implementation, get_abi3_version().unwrap_or(config.target_abi.version), )?; - if config.target_abi.kind.is_free_threaded() { + // only allow free-threaded builds if the build environment didn't force an abi3 build + if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } config.target_abi = abi_builder.finalize(); @@ -591,15 +592,28 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); - - let target_abi = if !(target_abi.is_some() || abi3.is_some()) { + let target_abi = if !(target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { PythonAbiBuilder::new(implementation, version).finalize() } else if abi3.is_some() && abi3.unwrap() { - // should we produce a user-visible warning about this? + warn!("abi3 configuration file option is deprecated, set target_abi instead"); PythonAbiBuilder::new(implementation, version) .abi3() .unwrap() .finalize() + } else if let Some(ref flags) = build_flags { + if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + } else { + // we could avoid this branch with if let chains + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) + } } else { ensure!( !(target_abi.is_some() && abi3.is_some()), @@ -737,7 +751,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } -#[cfg_attr(test, derive(Debug))] +#[derive(Debug)] pub struct InterpreterConfigBuilder { implementation: PythonImplementation, version: PythonVersion, @@ -775,7 +789,11 @@ impl InterpreterConfigBuilder { } pub fn target_abi(self, target_abi: PythonAbi) -> Result { - ensure!(self.target_abi.is_none(), "Target ABI already set!"); + ensure!( + self.target_abi.is_none(), + "Target ABI already set to {:?}", + target_abi + ); Ok(InterpreterConfigBuilder { target_abi: Some(target_abi), ..self @@ -946,7 +964,10 @@ impl PythonAbiBuilder { pub fn abi3(self) -> Result { if self.kind.is_some() { - bail!("Target ABI already chosen!") + bail!( + "ABI kind already set to {:?}, cannot set to abi3", + self.kind + ) } // PyPy and GraalPy don't support abi3; don't adjust the version @@ -972,7 +993,10 @@ impl PythonAbiBuilder { pub fn free_threaded(self) -> Result { if self.kind.is_some() { - bail!("Target ABI already chosen!") + bail!( + "Target ABI already set to {:?}, cannot set to free-threaded", + self.kind + ) } if self.version < PythonVersion::PY313 { let version = self.version; @@ -1569,8 +1593,8 @@ impl FromStr for BuildFlag { /// is the equivalent of `#ifdef {varname}` in C. /// /// see Misc/SpecialBuilds.txt in the python source for what these mean. -#[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[derive(Clone, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[derive(Debug, Clone, Default)] pub struct BuildFlags(pub HashSet); impl BuildFlags { @@ -1979,7 +2003,8 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result> { config.target_abi.implementation, get_abi3_version().unwrap_or(config.target_abi.version), )?; - if config.target_abi.kind.is_free_threaded() { + if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } config.target_abi = abi_builder.finalize(); From cde688c79da8096dbb248978210e7806fcad0adb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 15:32:22 -0600 Subject: [PATCH 09/20] fix build-config test --- pyo3-build-config/src/impl_.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 8f06734b9e3..98ae8db8afd 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -2985,7 +2985,7 @@ mod tests { assert!(builder .unwrap_err() .to_string() - .contains("ABI already chosen!")); + .contains("ABI kind already set to")); let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) .free_threaded(); From b11b262d5a193216af0f43c7550708466ad3a2f8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 24 Apr 2026 16:16:07 -0600 Subject: [PATCH 10/20] Fix issues linking against onld stable ABI versions --- pyo3-build-config/src/impl_.rs | 76 ++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 98ae8db8afd..5ad6c6dbe20 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -451,7 +451,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = PythonAbiBuilder::from_build_env(implementation, version)?; + let mut abi_builder = PythonAbiBuilder::from_build_env( + implementation, + version, + if is_abi3() { Some(version) } else { None }, + )?; if gil_disabled && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } @@ -495,9 +499,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse contents of PYO3_CONFIG_FILE")?; // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 // feature. + let abi3_version = get_abi3_version(); let mut abi_builder = PythonAbiBuilder::from_build_env( - config.target_abi.implementation, - get_abi3_version().unwrap_or(config.target_abi.version), + config.implementation, + config.version, + abi3_version, )?; // only allow free-threaded builds if the build environment didn't force an abi3 build if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { @@ -592,35 +598,38 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); - let target_abi = if !(target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { - PythonAbiBuilder::new(implementation, version).finalize() - } else if abi3.is_some() && abi3.unwrap() { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - PythonAbiBuilder::new(implementation, version) - .abi3() - .unwrap() - .finalize() - } else if let Some(ref flags) = build_flags { - if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + let target_abi = + if !(is_abi3() || target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { + PythonAbiBuilder::new(implementation, version).finalize() + } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { + if abi3.is_some() && abi3.unwrap() { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } PythonAbiBuilder::new(implementation, version) - .free_threaded() + .abi3() .unwrap() .finalize() + } else if let Some(ref flags) = build_flags { + if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + } else { + // we could avoid this branch with if let chains + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) + } } else { - // we could avoid this branch with if let chains ensure!( !(target_abi.is_some() && abi3.is_some()), "Invalid config that sets both target_abi and abi3." ); - target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) - } - } else { - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap() - }; + target_abi.unwrap() + }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) @@ -949,13 +958,14 @@ impl PythonAbiBuilder { pub fn from_build_env( implementation: PythonImplementation, version: PythonVersion, + abi3_version: Option, ) -> Result { let builder = PythonAbiBuilder { implementation, - version, + version: abi3_version.unwrap_or(version), kind: None, }; - if is_abi3() { + if abi3_version.is_some() && is_abi3() { builder.abi3() } else { Ok(builder) @@ -1271,9 +1281,6 @@ fn have_python_interpreter() -> bool { env_var("PYO3_NO_PYTHON").is_none() } -/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate. -/// -/// Must be called from a PyO3 crate build script. fn is_abi3() -> bool { cargo_env_var("CARGO_FEATURE_ABI3").is_some() || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") @@ -2002,7 +2009,11 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut config = load_cross_compile_config(cross_config)?; let mut abi_builder = PythonAbiBuilder::from_build_env( - config.target_abi.implementation, - get_abi3_version().unwrap_or(config.target_abi.version), + config.implementation, + config.version, + get_abi3_version(), )?; if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; From 9cb420ac5fa5224fdc4edcf168229c83889b5c07 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 14:40:51 -0600 Subject: [PATCH 11/20] remove load-bearing debug impls --- pyo3-build-config/src/impl_.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 5ad6c6dbe20..81f5c22063f 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -760,7 +760,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } } -#[derive(Debug)] +#[cfg_attr(test, derive(Debug))] pub struct InterpreterConfigBuilder { implementation: PythonImplementation, version: PythonVersion, @@ -800,7 +800,7 @@ impl InterpreterConfigBuilder { pub fn target_abi(self, target_abi: PythonAbi) -> Result { ensure!( self.target_abi.is_none(), - "Target ABI already set to {:?}", + "Target ABI already set to {}", target_abi ); Ok(InterpreterConfigBuilder { @@ -939,7 +939,7 @@ impl InterpreterConfigBuilder { } } -#[derive(Debug)] +#[cfg_attr(test, derive(Debug))] pub struct PythonAbiBuilder { implementation: PythonImplementation, version: PythonVersion, @@ -975,8 +975,8 @@ impl PythonAbiBuilder { pub fn abi3(self) -> Result { if self.kind.is_some() { bail!( - "ABI kind already set to {:?}, cannot set to abi3", - self.kind + "ABI kind already set to {}, cannot set to abi3", + self.kind.unwrap() ) } @@ -1004,8 +1004,8 @@ impl PythonAbiBuilder { pub fn free_threaded(self) -> Result { if self.kind.is_some() { bail!( - "Target ABI already set to {:?}, cannot set to free-threaded", - self.kind + "Target ABI already set to {}, cannot set to free-threaded", + self.kind.unwrap() ) } if self.version < PythonVersion::PY313 { @@ -1034,7 +1034,8 @@ impl PythonAbiBuilder { } #[non_exhaustive] -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] pub struct PythonAbi { /// The Python implementation flavor. /// @@ -1078,7 +1079,8 @@ impl FromStr for PythonAbi { } /// The "kind" of stable ABI. Either abi3 or abi3t currently. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] pub enum PythonAbiKind { /// The original stable ABI, supporting Python 3.2 and up Abi3, @@ -1128,7 +1130,8 @@ impl PythonAbiKind { } /// Whether the ABI is for the GIL-enabled or free-threaded build. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] pub enum GilUsed { /// The original PyObject layout GilEnabled, From 7fe98abccc040f0f9cde8a8fe9a1f8a63787e87d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 15:05:22 -0600 Subject: [PATCH 12/20] fix issue with InterpreterConfig::from_interpreter never returning an abi3 config --- pyo3-build-config/src/impl_.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 81f5c22063f..91e62764c9e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -355,6 +355,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut abi_builder = PythonAbiBuilder::new(implementation, target_version); + if is_abi3() || abi3_version.is_some() { + abi_builder = abi_builder.abi3()?; + } + if gil_disabled { abi_builder = abi_builder.free_threaded()?; } From 41be8375c7628094ca6432dc229159c267e94293 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 15:10:19 -0600 Subject: [PATCH 13/20] fix clippy lint --- pyo3-build-config/src/impl_.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 91e62764c9e..58607ad841c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -977,11 +977,8 @@ impl PythonAbiBuilder { } pub fn abi3(self) -> Result { - if self.kind.is_some() { - bail!( - "ABI kind already set to {}, cannot set to abi3", - self.kind.unwrap() - ) + if let Some(kind) = self.kind { + bail!("ABI kind already set to {kind}, cannot set to abi3",) } // PyPy and GraalPy don't support abi3; don't adjust the version @@ -1006,11 +1003,8 @@ impl PythonAbiBuilder { } pub fn free_threaded(self) -> Result { - if self.kind.is_some() { - bail!( - "Target ABI already set to {}, cannot set to free-threaded", - self.kind.unwrap() - ) + if let Some(kind) = self.kind { + bail!("Target ABI already set to {kind}, cannot set to free-threaded",) } if self.version < PythonVersion::PY313 { let version = self.version; From 58162a4805d041274240d2efb210814ef1c22609 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 15:34:32 -0600 Subject: [PATCH 14/20] fix PythonAbiBuilder::from_build_env --- pyo3-build-config/src/impl_.rs | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 58607ad841c..d7c6fa75733 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -353,11 +353,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) version }; - let mut abi_builder = PythonAbiBuilder::new(implementation, target_version); - - if is_abi3() || abi3_version.is_some() { - abi_builder = abi_builder.abi3()?; - } + let mut abi_builder = + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)?; if gil_disabled { abi_builder = abi_builder.free_threaded()?; @@ -455,11 +452,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = PythonAbiBuilder::from_build_env( - implementation, - version, - if is_abi3() { Some(version) } else { None }, - )?; + let mut abi_builder = + PythonAbiBuilder::from_build_env(implementation, version, Some(version))?; if gil_disabled && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } @@ -969,7 +963,7 @@ impl PythonAbiBuilder { version: abi3_version.unwrap_or(version), kind: None, }; - if abi3_version.is_some() && is_abi3() { + if is_abi3() { builder.abi3() } else { Ok(builder) @@ -2010,11 +2004,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Date: Tue, 28 Apr 2026 16:30:06 -0600 Subject: [PATCH 15/20] Only target abi3 for non-free-threaded builds --- pyo3-build-config/src/impl_.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index d7c6fa75733..5e244d19d35 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -353,8 +353,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) version }; - let mut abi_builder = - PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)?; + let mut abi_builder = if gil_disabled { + PythonAbiBuilder::new(implementation, target_version) + } else { + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? + }; if gil_disabled { abi_builder = abi_builder.free_threaded()?; @@ -599,14 +602,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let target_abi = if !(is_abi3() || target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { PythonAbiBuilder::new(implementation, version).finalize() - } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { - if abi3.is_some() && abi3.unwrap() { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - } - PythonAbiBuilder::new(implementation, version) - .abi3() - .unwrap() - .finalize() } else if let Some(ref flags) = build_flags { if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { PythonAbiBuilder::new(implementation, version) @@ -621,6 +616,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) } + } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { + if abi3.is_some() && abi3.unwrap() { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } + PythonAbiBuilder::new(implementation, version) + .abi3() + .unwrap() + .finalize() } else { ensure!( !(target_abi.is_some() && abi3.is_some()), From c8993904e6a76c13298c958f11aa34e93920c3c2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 28 Apr 2026 16:37:34 -0600 Subject: [PATCH 16/20] fix logic error in InterpreterConfig::from_reader --- pyo3-build-config/src/impl_.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 5e244d19d35..a44b1cfc6b0 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -455,12 +455,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let mut abi_builder = - PythonAbiBuilder::from_build_env(implementation, version, Some(version))?; - if gil_disabled && abi_builder.kind.is_none() { - abi_builder = abi_builder.free_threaded()?; + let target_abi = if gil_disabled { + PythonAbiBuilder::new(implementation, version).free_threaded()? + } else { + PythonAbiBuilder::from_build_env(implementation, version, Some(version))? } - let target_abi = abi_builder.finalize(); + .finalize(); let lib_name = Some(default_lib_name_unix( target_abi, cygwin, From ea2eab72baab81c909222c3c1c6a56f50ba4a860 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 13:33:38 -0600 Subject: [PATCH 17/20] Add stub abi3t implementation --- pyo3-build-config/src/impl_.rs | 275 +++++++++++++++++++++------------ pyo3-build-config/src/lib.rs | 1 + pyo3-ffi/build.rs | 27 ++-- 3 files changed, 197 insertions(+), 106 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index a44b1cfc6b0..0bedb4a720d 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -208,23 +208,27 @@ impl InterpreterConfig { } match self.target_abi.kind { - PythonAbiKind::Abi3 => { - if !self.target_abi.kind.is_free_threaded() { - out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + PythonAbiKind::Stable(kind) => { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + if kind == StableAbi::Abi3t { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + out.push("cargo:rustc-cfg=Py_TARGET_ABI3T".to_owned()); } } - PythonAbiKind::VersionSpecific(_) => {} + PythonAbiKind::VersionSpecific(kind) => match kind { + GilUsed::FreeThreaded => { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()); + } + GilUsed::GilEnabled => {} + }, } - for flag in &self.build_flags.0 { match flag { - BuildFlag::Py_GIL_DISABLED => { - out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()) - } + // already handled by target ABI logic above + BuildFlag::Py_GIL_DISABLED => continue, flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{flag}\"")), } } - out } @@ -599,38 +603,47 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); - let target_abi = - if !(is_abi3() || target_abi.is_some() || abi3.is_some() || build_flags.is_some()) { - PythonAbiBuilder::new(implementation, version).finalize() - } else if let Some(ref flags) = build_flags { - if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - PythonAbiBuilder::new(implementation, version) - .free_threaded() - .unwrap() - .finalize() - } else { - // we could avoid this branch with if let chains - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) - } - } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { - if abi3.is_some() && abi3.unwrap() { - warn!("abi3 configuration file option is deprecated, set target_abi instead"); - } + let target_abi = if !(is_abi3() + || is_abi3t() + || target_abi.is_some() + || abi3.is_some() + || build_flags.is_some()) + { + PythonAbiBuilder::new(implementation, version).finalize() + } else if is_abi3t() { + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .unwrap() + .finalize() + } else if let Some(ref flags) = build_flags { + if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { PythonAbiBuilder::new(implementation, version) - .abi3() + .free_threaded() .unwrap() .finalize() } else { + // we could avoid this branch with if let chains ensure!( !(target_abi.is_some() && abi3.is_some()), "Invalid config that sets both target_abi and abi3." ); - target_abi.unwrap() - }; + target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) + } + } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { + if abi3.is_some() && abi3.unwrap() { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + } else { + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap() + }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) @@ -810,13 +823,13 @@ impl InterpreterConfigBuilder { }) } - pub fn abi3(self) -> Result { + pub fn stable_abi(self, kind: StableAbi) -> Result { let implementation = self.implementation; let version = self.version; self.target_abi( PythonAbiBuilder::new(implementation, version) - .abi3() - // this can't panic because abi3() is caleld on a builder with no chosen ABI + .stable_abi(kind) + // this can't panic because stable_abi() is called on a builder with no chosen ABI .unwrap() .finalize(), ) @@ -885,11 +898,13 @@ impl InterpreterConfigBuilder { ensure!(self.build_flags.is_none(), "Build flags already set!"); let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { ensure!(self.target_abi.is_some(), "Must target a free-threaded ABI if build flags contain Py_GIL_DISABLED but no target_abi is set"); - ensure!( - self.target_abi.unwrap().kind - == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded), - "build_flags contains Py_GIL_DISABLED but target ABI is not free-threaded" - ); + if let Some(target_abi) = self.target_abi { + ensure!( + target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded) + || target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t), + "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" + ); + } build_flags } else if let Some(target_abi) = self.target_abi { let mut flags = build_flags.clone(); @@ -967,33 +982,31 @@ impl PythonAbiBuilder { kind: None, }; if is_abi3() { - builder.abi3() + builder.stable_abi(StableAbi::Abi3) } else { Ok(builder) } } - pub fn abi3(self) -> Result { - if let Some(kind) = self.kind { - bail!("ABI kind already set to {kind}, cannot set to abi3",) - } - - // PyPy and GraalPy don't support abi3; don't adjust the version - if self.implementation.is_pypy() || self.implementation.is_graalpy() { - return Ok(PythonAbiBuilder { - implementation: self.implementation, - version: self.version, - kind: self.kind, - }); - } + pub fn stable_abi(self, kind: StableAbi) -> Result { + ensure!( + self.kind.is_none(), + "ABI kind already set to {}, cannot set to {}", + self.kind.unwrap(), + kind + ); + ensure!( + kind == StableAbi::Abi3 || kind == StableAbi::Abi3t, + "Cannot set a stable abi build with a version-specific ABI kind '{kind}'" + ); let mut build_version = self.version; if self.version.minor > STABLE_ABI_MAX_MINOR { - warn!("Automatically falling back to abi3-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); + warn!("Automatically falling back to {kind}-py3{STABLE_ABI_MAX_MINOR} because current Python is higher than the maximum supported"); build_version.minor = STABLE_ABI_MAX_MINOR; } Ok(PythonAbiBuilder { - kind: Some(PythonAbiKind::Abi3), + kind: Some(PythonAbiKind::Stable(kind)), version: build_version, ..self }) @@ -1077,8 +1090,8 @@ impl FromStr for PythonAbi { #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] pub enum PythonAbiKind { - /// The original stable ABI, supporting Python 3.2 and up - Abi3, + /// One of the stable ABIs, which supports multiple Python versions + Stable(StableAbi), /// Version specific ABI, which is different on the free-threaded build VersionSpecific(GilUsed), } @@ -1086,7 +1099,7 @@ pub enum PythonAbiKind { impl Display for PythonAbiKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - PythonAbiKind::Abi3 => write!(f, "abi3"), + PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"), PythonAbiKind::VersionSpecific(gil_disabled) => { write!(f, "version_specific({gil_disabled})") } @@ -1099,7 +1112,8 @@ impl FromStr for PythonAbiKind { fn from_str(value: &str) -> Result { match value { - "abi3" => Ok(PythonAbiKind::Abi3), + "abi3" => Ok(PythonAbiKind::Stable(StableAbi::Abi3)), + "abi3t" => Ok(PythonAbiKind::Stable(StableAbi::Abi3t)), "version_specific(free_threaded)" => { Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)) } @@ -1115,15 +1129,30 @@ impl PythonAbiKind { pub fn is_free_threaded(&self) -> bool { match self { PythonAbiKind::VersionSpecific(gil_disabled) => *gil_disabled == GilUsed::FreeThreaded, - PythonAbiKind::Abi3 => false, + PythonAbiKind::Stable(StableAbi::Abi3) => false, + PythonAbiKind::Stable(StableAbi::Abi3t) => true, } } +} - pub fn is_abi3(&self) -> bool { - matches!(self, PythonAbiKind::Abi3) - } +/// Whether the ABI is for the GIL-enabled or free-threaded build. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub enum StableAbi { + /// The original stable ABI, supporting Python 3.2 and up + Abi3, + /// The free-threaded stable ABI, supporting Python 3.15 and up + Abi3t, } +impl Display for StableAbi { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StableAbi::Abi3 => write!(f, "abi3"), + StableAbi::Abi3t => write!(f, "abi3t"), + } + } +} /// Whether the ABI is for the GIL-enabled or free-threaded build. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(test, derive(Debug))] @@ -1284,6 +1313,14 @@ fn is_abi3() -> bool { || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") } +/// Checks if `abi3t` or any of the `abi3t-py3*` features is enabled for the PyO3 crate. +/// +/// Must be called from a PyO3 crate build script. +fn is_abi3t() -> bool { + cargo_env_var("CARGO_FEATURE_ABI3T").is_some() + || env_var("PYO3_USE_ABI3T_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") +} + /// Gets the minimum supported Python version from PyO3 `abi3-py*` features. /// /// Must be called from a PyO3 crate build script. @@ -1293,6 +1330,15 @@ pub fn get_abi3_version() -> Option { minor_version.map(|minor| PythonVersion { major: 3, minor }) } +/// Gets the minimum supported Python version from PyO3 `abi3t-py*` features. +/// +/// Must be called from a PyO3 crate build script. +pub fn get_abi3t_version() -> Option { + let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=STABLE_ABI_MAX_MINOR) + .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3T_PY3{i}")).is_some()); + minor_version.map(|minor| PythonVersion { major: 3, minor }) +} + /// Checks if the `extension-module` feature is enabled for the PyO3 crate. /// /// This can be triggered either by: @@ -2037,7 +2083,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap(); // abi3() sets the target_abi on the builder struct so unwrapping is safe let target_abi = builder.target_abi.unwrap(); @@ -2113,12 +2159,18 @@ fn default_lib_name_windows(abi: PythonAbi, mingw: bool, debug: bool) -> Result< "python{}{}_d", abi.version.major, abi.version.minor )) - } else if matches!(abi.kind, PythonAbiKind::Abi3) && !abi.implementation.is_graalpy() { - if debug { - Ok(WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned()) + } else if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3) + || abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) + { + let mut lib_name = if debug { + WINDOWS_STABLE_ABI_DEBUG_LIB_NAME.to_owned() } else { - Ok(WINDOWS_STABLE_ABI_LIB_NAME.to_owned()) + WINDOWS_STABLE_ABI_LIB_NAME.to_owned() + }; + if abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t) { + lib_name.push('t'); } + Ok(lib_name) } else if mingw { ensure!( !abi.kind.is_free_threaded(), @@ -2151,8 +2203,10 @@ fn default_lib_name_unix(abi: PythonAbi, cygwin: bool, ld_version: Option<&str>) PythonImplementation::CPython => match ld_version { Some(ld_version) => Ok(format!("python{ld_version}")), None => { - if cygwin && matches!(abi.kind, PythonAbiKind::Abi3) { + if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) { Ok("python3".to_string()) + } else if cygwin && matches!(abi.kind, PythonAbiKind::Stable(StableAbi::Abi3t)) { + Ok("python3t".to_string()) } else if abi.kind.is_free_threaded() { ensure!(abi.version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", abi.version.major, abi.version.minor); Ok(format!( @@ -2286,10 +2340,11 @@ pub fn find_interpreter() -> Result { /// Locates and extracts the build host Python interpreter configuration. /// /// Lowers the configured Python version to `abi3_version` if required. -fn get_host_interpreter(abi3_version: Option) -> Result { +fn get_host_interpreter(stable_abi_version: Option) -> Result { let interpreter_path = find_interpreter()?; - let interpreter_config = InterpreterConfig::from_interpreter(interpreter_path, abi3_version)?; + let interpreter_config = + InterpreterConfig::from_interpreter(interpreter_path, stable_abi_version)?; Ok(interpreter_config) } @@ -2326,13 +2381,20 @@ pub fn make_cross_compile_config() -> Result> { pub fn make_interpreter_config() -> Result { let host = Triple::host(); let abi3_version = get_abi3_version(); + let abi3t_version = get_abi3t_version(); + + ensure!( + !(abi3_version.is_some() && abi3t_version.is_some()), + "Cannot simultaneously enable abi3 and abi3t features" + ); // See if we can safely skip the Python interpreter configuration detection. // Unix stable ABI extension modules can usually be built without any interpreter. - let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); + let need_interpreter = + (abi3_version.is_none() && abi3t_version.is_none()) || require_libdir_for_target(&host); if have_python_interpreter() { - match get_host_interpreter(abi3_version) { + match get_host_interpreter(abi3_version.or(abi3t_version)) { Ok(interpreter_config) => return Ok(interpreter_config), // Bail if the interpreter configuration is required to build. Err(e) if need_interpreter => return Err(e), @@ -2344,8 +2406,8 @@ pub fn make_interpreter_config() -> Result { } } else { ensure!( - abi3_version.is_some(), - "An abi3-py3* feature must be specified when compiling without a Python interpreter." + abi3_version.is_some() || abi3t_version.is_some(), + "An abi3-py3* or abi3t-py3* feature must be specified when compiling without a Python interpreter." ); }; @@ -2398,7 +2460,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) .executable(Some("executable".into())) @@ -2436,7 +2498,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = MINIMUM_SUPPORTED_VERSION; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .pointer_width(32) .executable(Some("executable".into())) @@ -2629,7 +2691,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY38; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) .finalize(); @@ -2643,7 +2705,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(); assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); @@ -2768,7 +2830,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2790,7 +2852,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), true, @@ -2802,7 +2864,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2814,7 +2876,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2826,7 +2888,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2840,7 +2902,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2852,7 +2914,7 @@ mod tests { assert_eq!( super::default_lib_name_windows( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), false, @@ -2970,7 +3032,7 @@ mod tests { assert_eq!( super::default_lib_name_unix( PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), true, @@ -2986,7 +3048,7 @@ mod tests { let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) .free_threaded() .unwrap() - .abi3(); + .stable_abi(StableAbi::Abi3); assert!(builder.is_err()); assert!(builder .unwrap_err() @@ -3059,7 +3121,7 @@ mod tests { let config = InterpreterConfigBuilder::new(implementation, host_version) .target_abi( PythonAbiBuilder::new(implementation, target_version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .finalize(), ) @@ -3294,7 +3356,7 @@ mod tests { let implementation = PythonImplementation::CPython; let version = PythonVersion::PY39; let interpreter_config = InterpreterConfigBuilder::new(implementation, version) - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .lib_name(Some("python3".into())) .finalize(); @@ -3324,6 +3386,29 @@ mod tests { "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] ); + + let interpreter_config = InterpreterConfig { + target_abi: PythonAbi { + implementation: PythonImplementation::CPython, + version: PythonVersion::PY315, + ..interpreter_config.target_abi + }, + ..interpreter_config + }; + assert_eq!( + interpreter_config.build_script_outputs(), + [ + "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), + "cargo:rustc-cfg=Py_3_10".to_owned(), + "cargo:rustc-cfg=Py_3_11".to_owned(), + "cargo:rustc-cfg=Py_3_12".to_owned(), + "cargo:rustc-cfg=Py_3_13".to_owned(), + "cargo:rustc-cfg=Py_3_14".to_owned(), + "cargo:rustc-cfg=Py_3_15".to_owned(), + "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), + ] + ); } #[test] @@ -3366,12 +3451,12 @@ mod tests { let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); assert!(builder - .abi3() + .stable_abi(StableAbi::Abi3) .unwrap() .build_flags(flags) .unwrap_err() .to_string() - .contains("target ABI is not free-threaded")); + .contains("is not free-threaded")); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); @@ -3491,7 +3576,7 @@ mod tests { // abi3 config.target_abi = PythonAbi { - kind: PythonAbiKind::Abi3, + kind: PythonAbiKind::Stable(StableAbi::Abi3), ..config.target_abi }; config.lib_name = None; diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index a41eb442b56..db652952e5c 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -313,6 +313,7 @@ pub mod pyo3_build_script_impl { pub use crate::impl_::{ cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config, target_triple_from_env, InterpreterConfig, PythonAbi, PythonAbiKind, PythonVersion, + StableAbi, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 34300893461..b1df86fab6a 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -3,7 +3,7 @@ use pyo3_build_config::{ pyo3_build_script_impl::{ cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target, resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, - InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, + InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, StableAbi, }, warn, PythonImplementation, }; @@ -128,21 +128,26 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if let PythonAbiKind::Abi3 = interpreter_config.target_abi.kind { + if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi.kind { match interpreter_config.target_abi.implementation { - PythonImplementation::CPython => { - if interpreter_config.target_abi.kind.is_free_threaded() { - warn!( - "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." - ) + PythonImplementation::CPython => match abi { + StableAbi::Abi3t => { + bail!("Abi3t builds are not yet supported") } - } + StableAbi::Abi3 => { + if interpreter_config.target_abi.kind.is_free_threaded() { + warn!( + "The free-threaded build of CPython does not support abi3 so the build artifacts will be version-specific." + ) + } + } + }, PythonImplementation::PyPy => warn!( - "PyPy does not yet support abi3 so the build artifacts will be version-specific. \ - See https://github.com/pypy/pypy/issues/3397 for more information." + "PyPy does not yet support {abi} so the build artifacts will be version-specific. \ + See https://github.com/pypy/pypy/issues/3397 for more information." ), PythonImplementation::GraalPy => warn!( - "GraalPy does not support abi3 so the build artifacts will be version-specific." + "GraalPy does not support {abi} so the build artifacts will be version-specific." ), PythonImplementation::RustPython => {} } From 8354ef7cf11d54c3ceaca1a025bd0c849af01a79 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 14:50:09 -0600 Subject: [PATCH 18/20] Improve test coverage --- pyo3-build-config/src/impl_.rs | 182 ++++++++++++++++++++++----------- 1 file changed, 125 insertions(+), 57 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 0bedb4a720d..2cfa4f15da0 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -360,7 +360,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut abi_builder = if gil_disabled { PythonAbiBuilder::new(implementation, target_version) } else { - PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version)? + // we already + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version) }; if gil_disabled { @@ -460,9 +461,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let cygwin = soabi.ends_with("cygwin"); let target_abi = if gil_disabled { - PythonAbiBuilder::new(implementation, version).free_threaded()? + // unwrap is safe because this is a freshly created PythonAbiBuilder with no ABI kind set + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() } else { - PythonAbiBuilder::from_build_env(implementation, version, Some(version))? + PythonAbiBuilder::from_build_env(implementation, version, Some(version)) } .finalize(); let lib_name = Some(default_lib_name_unix( @@ -509,7 +513,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) config.implementation, config.version, abi3_version, - )?; + ); // only allow free-threaded builds if the build environment didn't force an abi3 build if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; @@ -603,32 +607,27 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); - let target_abi = if !(is_abi3() - || is_abi3t() - || target_abi.is_some() - || abi3.is_some() - || build_flags.is_some()) - { - PythonAbiBuilder::new(implementation, version).finalize() + let flags_contains_free_threaded = if let Some(ref flags) = build_flags { + flags.0.contains(&BuildFlag::Py_GIL_DISABLED) + } else { + false + }; + let target_abi = if target_abi.is_some() { + ensure!( + !(target_abi.is_some() && abi3.is_some()), + "Invalid config that sets both target_abi and abi3." + ); + target_abi.unwrap() } else if is_abi3t() { PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3t) .unwrap() .finalize() - } else if let Some(ref flags) = build_flags { - if flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - PythonAbiBuilder::new(implementation, version) - .free_threaded() - .unwrap() - .finalize() - } else { - // we could avoid this branch with if let chains - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap_or(PythonAbiBuilder::new(implementation, version).finalize()) - } + } else if flags_contains_free_threaded { + PythonAbiBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() } else if (abi3.is_some() && abi3.unwrap()) || is_abi3() { if abi3.is_some() && abi3.unwrap() { warn!("abi3 configuration file option is deprecated, set target_abi instead"); @@ -638,16 +637,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .unwrap() .finalize() } else { - ensure!( - !(target_abi.is_some() && abi3.is_some()), - "Invalid config that sets both target_abi and abi3." - ); - target_abi.unwrap() + PythonAbiBuilder::new(implementation, version).finalize() }; let build_flags = build_flags.unwrap_or_default(); let builder = InterpreterConfigBuilder::new(implementation, version) - .target_abi(target_abi)? + .target_abi(target_abi) + // cannot fail because this is a newly-created InterpreterConfigBuilder with no target ABI set + .unwrap() .shared(shared.unwrap_or(true)) .lib_name(lib_name) .lib_dir(lib_dir) @@ -844,7 +841,9 @@ impl InterpreterConfigBuilder { // this can't panic because abi3() is called on a builder with no chosen ABI .unwrap() .finalize(), - )? + ) + // this can't panic because abi3() is called on a builder with no chosen ABI + .unwrap() .build_flags(BuildFlags::default()) } @@ -897,11 +896,9 @@ impl InterpreterConfigBuilder { pub fn build_flags(self, build_flags: BuildFlags) -> Result { ensure!(self.build_flags.is_none(), "Build flags already set!"); let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { - ensure!(self.target_abi.is_some(), "Must target a free-threaded ABI if build flags contain Py_GIL_DISABLED but no target_abi is set"); if let Some(target_abi) = self.target_abi { ensure!( - target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded) - || target_abi.kind == PythonAbiKind::Stable(StableAbi::Abi3t), + target_abi.kind.is_free_threaded(), "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" ); } @@ -975,16 +972,17 @@ impl PythonAbiBuilder { implementation: PythonImplementation, version: PythonVersion, abi3_version: Option, - ) -> Result { + ) -> PythonAbiBuilder { let builder = PythonAbiBuilder { implementation, version: abi3_version.unwrap_or(version), kind: None, }; if is_abi3() { - builder.stable_abi(StableAbi::Abi3) + // freshly created builder without an ABI kind so this can't fail + builder.stable_abi(StableAbi::Abi3).unwrap() } else { - Ok(builder) + builder } } @@ -1172,18 +1170,6 @@ impl Display for GilUsed { } } -impl FromStr for GilUsed { - type Err = crate::errors::Error; - - fn from_str(value: &str) -> Result { - match value { - "gil_enabled" => Ok(GilUsed::GilEnabled), - "free_threaded" => Ok(GilUsed::FreeThreaded), - _ => Err(format!("Unrecognized ABI name: {value}").into()), - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct PythonVersion { pub major: u8, @@ -2053,7 +2039,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result> { config.implementation, config.version, get_abi3_version(), - )?; + ); if config.target_abi.kind.is_free_threaded() && abi_builder.kind.is_none() { abi_builder = abi_builder.free_threaded()?; } @@ -2537,6 +2523,70 @@ mod tests { ) } + #[test] + fn test_config_file_invalid_keys() { + assert!( + InterpreterConfig::from_reader("version=3.14\ntarget_abi=foo-bar-baz".as_bytes()) + .is_err() + ); + assert!(InterpreterConfig::from_reader( + "version=3.14\ntarget_abi=CPython-bar-baz".as_bytes() + ) + .is_err()); + assert!(InterpreterConfig::from_reader( + "version=3.14\ntarget_abi=CPython-abi3-baz".as_bytes() + ) + .is_err()); + } + + #[test] + fn gil_disabled_config_file_corner_cases() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + assert_eq!( + InterpreterConfig::from_reader("version=3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + ); + assert_eq!( + InterpreterConfig::from_reader( + "version=3.13\ntarget_abi=CPython-version_specific(free_threaded)-3.13".as_bytes() + ) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + ); + assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).unwrap_err().to_string().contains("is not free-threaded")) + } + + #[test] + fn abi3_from_old_config_file() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + assert_eq!( + InterpreterConfig::from_reader("version=3.13\nabi3=true".as_bytes()).unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + ); + } + + #[test] + fn test_target_abi_and_abi3() { + assert!(InterpreterConfig::from_reader( + "version=3.13\nabi3=true\ntarget_abi=CPython-abi3-3.13".as_bytes() + ) + .unwrap_err() + .to_string() + .contains("Invalid config"),); + } + #[test] fn build_flags_default() { assert_eq!(BuildFlags::default(), BuildFlags::new()); @@ -3060,6 +3110,22 @@ mod tests { assert!(builder.is_err()); assert!(builder.unwrap_err().to_string().contains("Cannot target")); + + assert_eq!( + PythonAbiBuilder::new( + PythonImplementation::CPython, + PythonVersion { + major: 3, + minor: 16, + }, + ) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + .version + .minor, + STABLE_ABI_MAX_MINOR + ); } #[test] @@ -3144,6 +3210,12 @@ mod tests { assert!(interpreter.unwrap_err().to_string().contains( "cannot set a minimum Python version 3.45 higher than the interpreter version" )); + + let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); + assert_eq!( + interpreter.unwrap().target_abi.version, + PythonVersion::PY313 + ); } #[test] @@ -3440,11 +3512,7 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder - .build_flags(flags) - .unwrap_err() - .to_string() - .contains("Must target a free-threaded ABI")); + assert!(builder.build_flags(flags).unwrap().target_abi.is_none()); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); From 6854756c258fe8ae7b7efeb74da4420c561920e6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 15:05:30 -0600 Subject: [PATCH 19/20] fix clippy --- pyo3-build-config/src/impl_.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 2cfa4f15da0..c6d5c53a9db 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -612,12 +612,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } else { false }; - let target_abi = if target_abi.is_some() { + let target_abi = if let Some(target_abi) = target_abi { ensure!( - !(target_abi.is_some() && abi3.is_some()), + abi3.is_none(), "Invalid config that sets both target_abi and abi3." ); - target_abi.unwrap() + target_abi } else if is_abi3t() { PythonAbiBuilder::new(implementation, version) .stable_abi(StableAbi::Abi3t) From 3a2166545a69da5b147ec9c054ba7475e989ae5c Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 30 Apr 2026 15:20:13 -0600 Subject: [PATCH 20/20] fix 3.14t abi3 builds being a no-op --- pyo3-build-config/src/impl_.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index c6d5c53a9db..1e8e596f74c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -897,10 +897,11 @@ impl InterpreterConfigBuilder { ensure!(self.build_flags.is_none(), "Build flags already set!"); let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { if let Some(target_abi) = self.target_abi { - ensure!( - target_abi.kind.is_free_threaded(), - "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" - ); + if !target_abi.kind.is_free_threaded() { + warn!( + "build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded" + ); + } } build_flags } else if let Some(target_abi) = self.target_abi { @@ -2561,7 +2562,9 @@ mod tests { .unwrap() .finalize() ); - assert!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).unwrap_err().to_string().contains("is not free-threaded")) + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert_eq!(InterpreterConfig::from_reader("version=3.13\ntarget_abi=CPython-version_specific(gil_enabled)-3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()).unwrap(), InterpreterConfigBuilder::new(implementation, version).build_flags(flags).unwrap().finalize()); } #[test] @@ -3518,13 +3521,17 @@ mod tests { InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); let mut flags = BuildFlags::new(); flags.0.insert(BuildFlag::Py_GIL_DISABLED); - assert!(builder - .stable_abi(StableAbi::Abi3) - .unwrap() - .build_flags(flags) - .unwrap_err() - .to_string() - .contains("is not free-threaded")); + assert!( + builder + .stable_abi(StableAbi::Abi3) + .unwrap() + .build_flags(flags) + .unwrap() + .finalize() + .target_abi + .kind + == PythonAbiKind::Stable(StableAbi::Abi3) + ); let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314);