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. diff --git a/noxfile.py b/noxfile.py index 64be1c48f50..0e0470e0039 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 """ diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 6a090d93b75..8e5dd966c76 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! { @@ -92,26 +92,17 @@ pub fn target_triple_from_env() -> Triple { /// strategies are used to populate this type. #[cfg_attr(test, derive(Debug, PartialEq, Eq))] pub struct InterpreterConfig { - /// The Python implementation flavor. + /// Which abi the build is configured to link against /// - /// Serialized to `implementation`. - pub implementation: PythonImplementation, - - /// Python `X.Y` version. e.g. `3.9`. - /// - /// 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 `abi3`. - pub abi3: bool, - /// The name of the link library defining Python. /// /// This effectively controls the `cargo:rustc-link-lib=` value to @@ -179,23 +170,27 @@ 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()), } - // 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.abi.kind { + PythonAbiKind::Abi3 => { + if !self.abi.kind.is_free_threaded() { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + } + } + PythonAbiKind::VersionSpecific(_) => {} } for flag in &self.build_flags.0 { @@ -211,7 +206,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 @@ -309,8 +307,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse minor version")?, }; - let abi3 = is_abi3(); - let implementation = map["implementation"].parse()?; let gil_disabled = match map["gil_disabled"].as_str() { @@ -320,29 +316,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, - abi3, + 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, - abi3, - 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) { @@ -362,10 +357,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .context("failed to parse calcsize_pointer")?; Ok(InterpreterConfig { - version, - implementation, shared, - abi3, + abi, lib_name: Some(lib_name), lib_dir, executable: map.get("executable").cloned(), @@ -420,14 +413,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let abi3 = is_abi3(); + 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, - abi3, + abi, cygwin, sysconfigdata.get_value("LDVERSION"), - gil_disabled, )?); let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") .map(|bytes_width: u32| bytes_width * 8) @@ -435,10 +430,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, - abi3, lib_dir, lib_name, executable: None, @@ -469,11 +462,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.abi3 |= is_abi3(); - 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) }) @@ -512,10 +507,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 abi3 = None; let mut lib_name = None; let mut lib_dir = None; let mut executable = None; @@ -537,10 +530,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), - "abi3" => parse_value!(abi3, value), "lib_name" => parse_value!(lib_name, value), "lib_dir" => parse_value!(lib_dir, value), "executable" => parse_value!(executable, value), @@ -557,16 +548,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 abi3 = abi3.unwrap_or(false); let build_flags = build_flags.unwrap_or_default(); Ok(InterpreterConfig { - implementation, - version, + abi: abi.unwrap(), shared: shared.unwrap_or(true), - abi3, lib_name, lib_dir, executable, @@ -586,13 +572,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.abi3, - self.is_free_threaded(), - target, - )); + self.lib_name = Some(default_lib_name_for_target(self.abi, target)); } } @@ -641,10 +621,8 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; } - write_line!(implementation)?; - write_line!(version)?; + write_line!(abi)?; write_line!(shared)?; - write_line!(abi3)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -690,39 +668,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(()); +#[derive(Debug)] +pub struct PythonAbiBuilder { + implementation: PythonImplementation, + version: PythonVersion, + kind: Option, +} + +impl PythonAbiBuilder { + pub fn new(implementation: PythonImplementation, version: PythonVersion) -> PythonAbiBuilder { + PythonAbiBuilder { + implementation, + version, + kind: None, + } + } + + 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, ); + 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"); + build_version.minor = STABLE_ABI_MAX_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; + 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 }) } + } - Ok(()) + 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, + } } } @@ -737,7 +857,7 @@ impl PythonVersion { major: 3, minor: 15, }; - pub const PY313: Self = PythonVersion { + pub(crate) const PY313: Self = PythonVersion { major: 3, minor: 13, }; @@ -745,10 +865,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 { @@ -849,7 +975,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 }) } @@ -1564,27 +1690,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 abi3 = true; + 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, - abi3, - false, - false, - false, - )?) + Some(default_lib_name_windows(abi, false, false)?) } else { None }; Ok(InterpreterConfig { - implementation, - version, + abi, shared: true, - abi3, lib_name, lib_dir: None, executable: None, @@ -1669,102 +1784,94 @@ fn load_cross_compile_config( } // These contains only the limited ABI symbols. -const WINDOWS_ABI3_LIB_NAME: &str = "python3"; -const WINDOWS_ABI3_DEBUG_LIB_NAME: &str = "python3_d"; +const WINDOWS_STABLE_ABI_LIB_NAME: &str = "python3"; +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, - abi3: bool, - 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, abi3, false, false, gil_disabled).unwrap() + default_lib_name_windows(abi, false, false).unwrap() } else { default_lib_name_unix( - version, - implementation, - abi3, + abi, target.operating_system == OperatingSystem::Cygwin, None, - gil_disabled, ) .unwrap() } } -fn default_lib_name_windows( - version: PythonVersion, - implementation: PythonImplementation, - abi3: bool, - 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 abi3 && !(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_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!( - !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, - abi3: bool, - 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 && abi3 { + 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()), @@ -1886,8 +1993,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) } @@ -1899,9 +2005,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 }; @@ -1917,7 +2029,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() { @@ -1926,7 +2038,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."); } @@ -1984,16 +2096,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 { - abi3: true, + 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, @@ -2004,9 +2118,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 { - abi3: false, + abi: abi_builder.finalize(), build_flags: { let mut flags = HashSet::new(); flags.insert(BuildFlag::Py_DEBUG); @@ -2015,14 +2129,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, @@ -2036,15 +2145,16 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { let config = InterpreterConfig { - abi3: true, + 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, @@ -2061,12 +2171,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, - abi3: false, lib_name: None, lib_dir: None, executable: None, @@ -2083,13 +2193,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, - abi3: false, lib_name: None, lib_dir: None, executable: None, @@ -2189,15 +2300,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + 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, @@ -2219,15 +2329,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + 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, @@ -2246,15 +2355,14 @@ mod tests { assert_eq!( InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(), InterpreterConfig { - abi3: false, + 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, @@ -2270,10 +2378,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, - abi3: true, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -2294,10 +2403,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, - abi3: true, lib_name: None, lib_dir: None, executable: None, @@ -2329,10 +2439,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, - abi3: false, lib_name: Some("python38".into()), lib_dir: Some("C:\\some\\path".into()), executable: None, @@ -2364,10 +2473,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, - abi3: false, lib_name: Some("python38".into()), lib_dir: Some("/usr/lib/mingw".into()), executable: None, @@ -2399,10 +2507,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, - abi3: false, lib_name: Some("python3.9".into()), lib_dir: Some("/usr/arm64/lib".into()), executable: None, @@ -2433,13 +2540,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, - abi3: false, lib_name: Some("pypy3.11-c".into()), lib_dir: None, executable: None, @@ -2454,34 +2557,28 @@ mod tests { #[test] fn default_lib_name_windows() { - use PythonImplementation::*; assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, false, ) .unwrap(), "python39", ); - assert!(super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, - 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, - true, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2490,23 +2587,21 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + 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, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), true, - true, - false, false, ) .unwrap(), @@ -2514,10 +2609,10 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - PyPy, - true, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2526,13 +2621,10 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - false, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .abi3(None) + .unwrap() + .finalize(), false, false, ) @@ -2541,12 +2633,12 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .abi3(None) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), "python39_d", @@ -2555,128 +2647,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, - true, + 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, - true, + 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, - false, - 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, - false, + 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, - false, + 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, - true, // 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, - false, - false, - true, - true, - ) - .unwrap(), "python313t_d", ); } #[test] fn default_lib_name_unix() { - use PythonImplementation::*; // Defaults to pythonX.Y for CPython 3.8+ assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 8 }, - CPython, - false, + 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, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, None, - false ) .unwrap(), "python3.9", @@ -2684,12 +2731,10 @@ mod tests { // Can use ldversion to override for CPython assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .finalize(), false, Some("3.8d"), - false ) .unwrap(), "python3.8d", @@ -2698,15 +2743,9 @@ mod tests { // PyPy 3.11 includes ldversion assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311).finalize(), false, None, - false ) .unwrap(), "pypy3.11-c", @@ -2714,12 +2753,9 @@ mod tests { assert_eq!( super::default_lib_name_unix( - PythonVersion { major: 3, minor: 9 }, - PyPy, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39).finalize(), false, Some("3.11d"), - false ) .unwrap(), "pypy3.11d-c", @@ -2728,44 +2764,25 @@ mod tests { // free-threading adds a t suffix assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - false, + 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, - false, - false, - None, - true, - ) - .is_err()); // cygwin abi3 links to unversioned libpython assert_eq!( super::default_lib_name_unix( - PythonVersion { - major: 3, - minor: 13 - }, - CPython, - true, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .abi3(None) + .unwrap() + .finalize(), true, None, - false ) .unwrap(), "python3", @@ -2825,47 +2842,34 @@ mod tests { #[test] fn interpreter_version_reduced_to_abi3() { - let mut config = InterpreterConfig { - abi3: true, + 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 { - abi3: true, - 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( @@ -2915,7 +2919,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, @@ -3047,13 +3051,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, - abi3: false, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3074,7 +3074,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.abi + }, ..interpreter_config }; assert_eq!( @@ -3092,10 +3095,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, - abi3: true, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3116,7 +3120,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.abi + }, ..interpreter_config }; assert_eq!( @@ -3135,13 +3142,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, - abi3: false, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3171,10 +3176,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, - abi3: false, lib_name: Some("python3".into()), lib_dir: None, executable: None, @@ -3226,10 +3230,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, - abi3: false, lib_name: None, lib_dir: None, executable: None, @@ -3256,8 +3259,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, }; @@ -3269,11 +3272,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, }; @@ -3292,7 +3295,10 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.abi3 = true; + 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 65d7f1bade4..674577550ba 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; @@ -267,13 +267,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 { @@ -310,7 +310,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, InterpreterConfig, PythonAbi, PythonAbiKind, PythonVersion, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. @@ -389,12 +389,12 @@ 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", }; - 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\ @@ -483,13 +483,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, - abi3: false, lib_name: None, lib_dir: None, executable: None, @@ -526,13 +522,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, - abi3: false, 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..3c6eb29cfb9 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; @@ -152,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 @@ -170,7 +172,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 127874ffa84..9e85631384b 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, PythonVersion, + InterpreterConfig, MaximumVersionExceeded, PythonAbiKind, PythonVersion, }, warn, PythonImplementation, }; @@ -45,32 +45,35 @@ 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); - 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.", - ); + 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.", + )); return Err(error.finish().into()); } @@ -81,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()); } @@ -111,25 +114,25 @@ 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()); } } } - if interpreter_config.abi3 { - 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 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." ) } }