diff --git a/newsfragments/5924.added.md b/newsfragments/5924.added.md new file mode 100644 index 00000000000..6d8e93af861 --- /dev/null +++ b/newsfragments/5924.added.md @@ -0,0 +1,7 @@ +* Added a PythonAbi struct, along with PythonAbiKind, StableAbi, and GilUsed +enums to pyo3-build-config. These types allow specifying the Python ABI to use +for a build. You can construct a PythonAbi struct using a PythonAbiBuilder +struct. + +* Added an InterpreterConfigBuilder struct for building InterpreterConfig +structs with a more ergonomic syntax. diff --git a/newsfragments/5924.changed.md b/newsfragments/5924.changed.md new file mode 100644 index 00000000000..195b8408e77 --- /dev/null +++ b/newsfragments/5924.changed.md @@ -0,0 +1,6 @@ +* The boolean `abi3` field of `pyo3_build_config::impl_::InterpreterConfig` is now + deprecated and has been replaced by a target_abi field, which is a `PythonAbi` + struct. +* The InterpreterConfig::from_interpreter function now accepts a second + `abi3_version` argument. The old behavior is the same as passing + `abi3_version = None` . diff --git a/noxfile.py b/noxfile.py index b7130b30d4c..e2c34066962 100644 --- a/noxfile.py +++ b/noxfile.py @@ -100,7 +100,7 @@ def test(session: nox.Session) -> None: @nox.session(name="test-rust", venv_backend="none") def test_rust(session: nox.Session): - _run_cargo_test(session, package="pyo3-build-config") + _run_cargo_test(session, package="pyo3-build-config", features="resolve-config") _run_cargo_test(session, package="pyo3-macros-backend") _run_cargo_test(session, package="pyo3-macros") diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9d6a4666eb5..3707bb8995d 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,8 +83,47 @@ pub fn target_triple_from_env() -> Triple { .expect("Unrecognized TARGET environment variable value") } +fn sanitize_abi3_version( + abi3_version: Option, + version: PythonVersion, +) -> Result { + 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 + ); + Ok(min_version) + } else { + Ok(version) + } +} + +fn apply_build_env_to_config(mut config: InterpreterConfig) -> Result { + let abi3_version = get_abi3_version(); + let mut abi_builder = + PythonAbiBuilder::from_build_env(config.implementation, config.version, abi3_version)?; + // only allow free-threaded builds if the build environment didn't force an abi3 build + if abi_builder.kind.is_none() { + if let PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded) = config.target_abi.kind { + abi_builder = abi_builder.free_threaded()?; + } else if let PythonAbiKind::Stable(stable_abi) = config.target_abi.kind { + abi_builder = abi_builder.stable_abi(stable_abi)?; + } + } + config.target_abi = abi_builder.finalize(); + Ok(config) +} + /// Configuration needed by PyO3 to build for the correct Python implementation. /// +/// The version and implementation fields on the correspond to the interpreter +/// used to host a build. These need not be the same as the implementation and +/// version set for the build target in the `target_abi` field. +/// /// Usually this is queried directly from the Python interpreter, or overridden using the /// `PYO3_CONFIG_FILE` environment variable. /// @@ -92,12 +131,12 @@ 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. + /// The host Python implementation flavor. /// /// Serialized to `implementation`. pub implementation: PythonImplementation, - /// Python `X.Y` version. e.g. `3.9`. + /// The host Python `X.Y` version. e.g. `3.9`. /// /// Serialized to `version`. pub version: PythonVersion, @@ -107,9 +146,22 @@ pub struct InterpreterConfig { /// Serialized to `shared`. pub shared: bool, - /// Whether linking against the stable/limited Python 3 API. + /// 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`, + /// `implementation` set to `PythonImplementation::CPython` and `version` set + /// to `PythonVersion {major: 3, minor: 9}` is equivalent to setting `target_abi` + /// to `PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion + /// {major: 3, minor: 9}).stable_abi(StableAbi::Abi3)?.finalize()`. /// - /// Serialized to `abi3`. + /// This field is not serialized, only read for backwards compatibility. + #[deprecated(note = "Set a target_abi that is abi3 instead")] pub abi3: bool, /// The name of the link library defining Python. @@ -179,40 +231,50 @@ 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.target_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.target_abi.version.minor { out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } - match self.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()), } - // 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.target_abi.kind { + PythonAbiKind::Stable(kind) => { + out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); + if kind == StableAbi::Abi3t { + panic!("Abi3t builds are not yet supported"); + } + } + 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 } #[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 @@ -310,8 +372,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() { @@ -321,28 +381,26 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) _ => panic!("Unknown Py_GIL_DISABLED value"), }; + let target_abi = + PythonAbiBuilder::from_sysconfig(implementation, version, abi3_version, gil_disabled)? + .finalize(); + let cygwin = map["cygwin"].as_str() == "True"; let lib_name = if cfg!(windows) { default_lib_name_windows( - version, - implementation, - abi3, + 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 // `_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, + target_abi, cygwin, map.get("ld_version").map(String::as_str), - gil_disabled, )? }; @@ -362,20 +420,16 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) .parse() .context("failed to parse calcsize_pointer")?; - Ok(InterpreterConfig { - version, - implementation, - shared, - abi3, - 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, - }) + 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(Some(calcsize_pointer * 8)) + .build_flags(BuildFlags::from_interpreter(interpreter)?)? + .python_framework_prefix(python_framework_prefix) + .finalize() } /// Generate from parsed sysconfigdata file @@ -421,34 +475,27 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) None => false, }; let cygwin = soabi.ends_with("cygwin"); - let abi3 = is_abi3(); + let target_abi = + PythonAbiBuilder::from_sysconfig(implementation, version, None, gil_disabled)? + .finalize(); let lib_name = Some(default_lib_name_unix( - version, - implementation, - abi3, + target_abi, cygwin, sysconfigdata.get_value("LDVERSION"), - gil_disabled, )?); let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") - .map(|bytes_width: u32| bytes_width * 8) - .ok(); + .map(|bytes_width: u32| Some(bytes_width * 8))?; let build_flags = BuildFlags::from_sysconfigdata(sysconfigdata); - Ok(InterpreterConfig { - implementation, - version, - shared: shared || framework, - abi3, - lib_dir, - lib_name, - executable: None, - pointer_width, - build_flags, - suppress_build_script_link_lines: false, - extra_build_script_lines: vec![], - python_framework_prefix, - }) + 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. @@ -466,17 +513,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "PYO3_CONFIG_FILE must be an absolute path" ); - let mut config = InterpreterConfig::from_path(path) - .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())?; - - Ok(config) + apply_build_env_to_config( + InterpreterConfig::from_path(path) + .context("failed to parse contents of PYO3_CONFIG_FILE")?, + ) }) } @@ -514,9 +554,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } let mut implementation = None; - let mut version = None; + let mut version: Option = None; let mut shared = None; - let mut abi3 = 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; @@ -541,6 +583,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), + "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), @@ -560,23 +603,59 @@ 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(); + 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 let Some(target_abi) = target_abi { + ensure!( + abi3.is_none(), + "Invalid config that sets both target_abi and abi3." + ); + target_abi + } else if is_abi3t() { + ensure!( + !is_abi3(), + "Cannot simultaneously enable features that enable abi3 and abi3t builds" + ); + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3t) + .unwrap() + .finalize() + } else if flags_contains_free_threaded { + // This fires even if is_abi3() is True for backward compatibility reasons + PythonAbiBuilder::new(implementation, version) + .free_threaded()? + .finalize() + } else if (abi3 == Some(true)) || is_abi3() { + if abi3 == Some(true) { + warn!("abi3 configuration file option is deprecated, set target_abi instead"); + } + PythonAbiBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + } else { + PythonAbiBuilder::new(implementation, version).finalize() + }; - Ok(InterpreterConfig { - implementation, - version, - shared: shared.unwrap_or(true), - abi3, - 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, - }) + let build_flags = build_flags.unwrap_or_default(); + let builder = InterpreterConfigBuilder::new(implementation, version) + .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) + .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) + .pointer_width(pointer_width); + + builder.finalize() } /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. @@ -587,13 +666,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.target_abi, target)); } } @@ -645,7 +718,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) write_line!(implementation)?; write_line!(version)?; write_line!(shared)?; - write_line!(abi3)?; + write_line!(target_abi)?; write_option_line!(lib_name)?; write_option_line!(lib_dir)?; write_option_line!(executable)?; @@ -693,37 +766,433 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) } 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.target_abi.kind.is_free_threaded() + } +} + +#[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: Vec, + 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: vec![], + python_framework_prefix: None, } + } - 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, - ); + pub fn target_abi(self, target_abi: PythonAbi) -> Result { + ensure!( + self.target_abi.is_none(), + "Target ABI already set to {}", + target_abi + ); + Ok(InterpreterConfigBuilder { + target_abi: Some(target_abi), + ..self + }) + } - 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; + pub fn stable_abi(self, kind: StableAbi) -> Result { + let implementation = self.implementation; + let version = self.version; + self.target_abi( + PythonAbiBuilder::new(implementation, version) + .stable_abi(kind)? + .finalize(), + ) + } + + pub fn free_threaded(self) -> Result { + let implementation = self.implementation; + let version = self.version; + self.target_abi( + PythonAbiBuilder::new(implementation, version) + .free_threaded()? + .finalize(), + ) + } + + pub fn lib_name(self, lib_name: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { lib_name, ..self } + } + + pub fn pointer_width(self, pointer_width: Option) -> InterpreterConfigBuilder { + InterpreterConfigBuilder { + pointer_width, + ..self } + } - Ok(()) + 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, + ..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!"); + 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) -> Result { + let mut build_flags = self.build_flags.unwrap_or_default(); + let py_gil_disabled = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); + let target_abi = match (self.target_abi, py_gil_disabled) { + // No target ABI set, no Py_GIL_DISABLED: default to GIL-enabled version-specific. + (None, false) => PythonAbiBuilder::new(self.implementation, self.version).finalize(), + // No target ABI set, Py_GIL_DISABLED in build flags: infer free-threaded. + (None, true) => PythonAbiBuilder::new(self.implementation, self.version) + .free_threaded()? + .finalize(), + // Target ABI set, no Py_GIL_DISABLED: use as-is. + (Some(target_abi), false) => target_abi, + // Target ABI set + Py_GIL_DISABLED: reconcile. + (Some(target_abi), true) => match target_abi.kind { + // abi3 + Py_GIL_DISABLED: the abi3 feature is a no-op on free-threaded + // interpreters, so for backward compatibility fall back to a free-threaded + // version-specific build. + PythonAbiKind::Stable(StableAbi::Abi3) => { + let new_abi = + PythonAbiBuilder::new(target_abi.implementation, target_abi.version) + .free_threaded()? + .finalize(); + warn!( + "Targeting an abi3 build but build_flags contains Py_GIL_DISABLED, \ + falling back to a version-specific free-threaded build" + ); + new_abi + } + // GIL-enabled version-specific + Py_GIL_DISABLED is contradictory. + PythonAbiKind::VersionSpecific(GilUsed::GilEnabled) => bail!( + "build_flags contains Py_GIL_DISABLED but target_abi \ + '{target_abi}' is not free-threaded" + ), + // Already free-threaded (Stable(Abi3t) or VersionSpecific(FreeThreaded)). + _ => target_abi, + }, + }; + if target_abi.kind.is_free_threaded() { + build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + } + #[allow(deprecated)] + Ok(InterpreterConfig { + implementation: self.implementation, + version: self.version, + shared: self.shared.unwrap_or(true), + target_abi, + abi3: matches!(target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)), + lib_name: self.lib_name, + lib_dir: self.lib_dir, + executable: self.executable, + pointer_width: self.pointer_width, + build_flags, + suppress_build_script_link_lines: self + .suppress_build_script_link_lines + .unwrap_or(false), + extra_build_script_lines: self.extra_build_script_lines, + python_framework_prefix: self.python_framework_prefix, + }) + } +} + +#[cfg_attr(test, 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 from_build_env( + implementation: PythonImplementation, + version: PythonVersion, + abi3_version: Option, + ) -> Result { + let builder = PythonAbiBuilder { + implementation, + version: sanitize_abi3_version(abi3_version, version)?, + kind: None, + }; + if is_abi3() { + builder.stable_abi(StableAbi::Abi3) + } else { + Ok(builder) + } + } + + pub fn from_sysconfig( + implementation: PythonImplementation, + version: PythonVersion, + abi3_version: Option, + gil_disabled: bool, + ) -> Result { + let target_version = sanitize_abi3_version(abi3_version, version)?; + if gil_disabled { + // TODO: fall back to abi3t builds on 3.15t and newer + PythonAbiBuilder::new(implementation, target_version).free_threaded() + } else { + PythonAbiBuilder::from_build_env(implementation, target_version, abi3_version) + } + } + + 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 + ); + let mut build_version = self.version; + if self.version.minor > STABLE_ABI_MAX_MINOR { + 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::Stable(kind)), + version: build_version, + ..self + }) + } + + pub fn free_threaded(self) -> Result { + ensure!( + self.kind.is_none(), + "Target ABI already set to {}, cannot set to free-threaded", + self.kind.unwrap() + ); + if self.version < PythonVersion::PY313 { + let version = self.version; + bail!( + "Cannot target free-threaded builds for Python versions before 3.13, tried to build for {version}" + ) + } + Ok(PythonAbiBuilder { + 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(GilUsed::GilEnabled)); + PythonAbi { + implementation: self.implementation, + kind, + version: self.version, + } + } +} + +#[non_exhaustive] +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +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 { + write!(f, "{}-{}-{}", self.implementation, self.kind, self.version) + } +} + +impl FromStr for PythonAbi { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + let mut parts = value.splitn(3, '-'); + Ok(PythonAbi { + implementation: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + kind: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + version: parts + .next() + .ok_or_else(|| format!("Invalid ABI string representation: {value}"))? + .parse()?, + }) + } +} + +/// The "kind" of ABI. +/// +/// Either a variety of stable ABI or a GIL-enabled or free-threaded +/// version-specific ABI. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(test, derive(Debug))] +pub enum PythonAbiKind { + /// 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), +} + +impl Display for PythonAbiKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PythonAbiKind::Stable(stable_abi) => write!(f, "{stable_abi}"), + PythonAbiKind::VersionSpecific(gil_used) => { + write!(f, "{gil_used}") + } + } + } +} + +impl FromStr for PythonAbiKind { + type Err = crate::errors::Error; + + fn from_str(value: &str) -> Result { + match value { + "abi3" => Ok(PythonAbiKind::Stable(StableAbi::Abi3)), + "abi3t" => Ok(PythonAbiKind::Stable(StableAbi::Abi3t)), + "free_threaded" => Ok(PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)), + "gil_enabled" => Ok(PythonAbiKind::VersionSpecific(GilUsed::GilEnabled)), + _ => Err(format!("Unrecognized ABI name: {value}").into()), + } + } +} + +impl PythonAbiKind { + pub fn is_free_threaded(self) -> bool { + match self { + PythonAbiKind::VersionSpecific(gil_disabled) => gil_disabled == GilUsed::FreeThreaded, + PythonAbiKind::Stable(StableAbi::Abi3) => false, + PythonAbiKind::Stable(StableAbi::Abi3t) => true, + } + } +} + +/// The the variety of stable ABI +#[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))] +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"), + } } } @@ -738,6 +1207,10 @@ impl PythonVersion { major: 3, minor: 15, }; + pub const PY314: Self = PythonVersion { + major: 3, + minor: 14, + }; pub const PY313: Self = PythonVersion { major: 3, minor: 13, @@ -746,10 +1219,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,15 +1328,32 @@ 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. 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 }) } +/// 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: @@ -1568,42 +2064,48 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result, + abi3t_version: Option, +) -> Result { + if abi3_version.is_some() && abi3t_version.is_some() { + bail!("Cannot simultaneously set abi3 and abi3t features") + } else if let Some(version) = abi3_version { + default_abi3_config(host, version) + } else if abi3t_version.is_some() { + bail!("Abi3t support has not yet been implemented") + } else { + bail!("Neither abi3 or abi3t features are enabled") + } +} + +/// Generates "default" interpreter configuration when compiling "abi3" extensions +/// without a working Python interpreter. +/// +/// `version` specifies the minimum supported Stable ABI CPython version. /// /// This should work for most CPython extension modules when compiling on /// Windows, macOS and Linux. @@ -1611,36 +2113,17 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. - let implementation = PythonImplementation::CPython; - let abi3 = true; - - let lib_name = if host.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows( - version, - implementation, - abi3, - false, - false, - false, - )?) + let target_abi = PythonAbiBuilder::new(PythonImplementation::CPython, version) + .stable_abi(StableAbi::Abi3)? + .finalize(); + let builder = InterpreterConfigBuilder::new(PythonImplementation::CPython, version) + .target_abi(target_abi)?; + if host.operating_system == OperatingSystem::Windows { + builder.lib_name(Some(default_lib_name_windows(target_abi, false, false)?)) } else { - None - }; - - Ok(InterpreterConfig { - implementation, - version, - shared: true, - abi3, - 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 @@ -1673,102 +2156,102 @@ 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()) { - if debug { - Ok(WINDOWS_ABI3_DEBUG_LIB_NAME.to_owned()) + Ok(format!( + "python{}{}_d", + abi.version.major, abi.version.minor + )) + } 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_ABI3_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!( - !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::Stable(StableAbi::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 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!( + "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()), @@ -1894,11 +2377,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 mut interpreter_config = InterpreterConfig::from_interpreter(interpreter_path)?; - interpreter_config.fixup_for_abi3_version(abi3_version)?; + let interpreter_config = + InterpreterConfig::from_interpreter(interpreter_path, stable_abi_version)?; Ok(interpreter_config) } @@ -1910,9 +2393,9 @@ 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) + Some(apply_build_env_to_config(load_cross_compile_config( + cross_config, + )?)?) } else { None }; @@ -1926,30 +2409,27 @@ 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(); // See if we can safely skip the Python interpreter configuration detection. - // Unix "abi3" extension modules can usually be built without any interpreter. - let need_interpreter = abi3_version.is_none() || require_libdir_for_target(&host); + // Unix stable ABI extension modules can usually be built without any interpreter. + 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), _ => { - // 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."); } } - } else { - ensure!( - abi3_version.is_some(), - "An abi3-py3* feature must be specified when compiling without a Python interpreter." - ); - }; + } - let interpreter_config = default_abi3_config(&host, abi3_version.unwrap())?; + let interpreter_config = default_stable_abi_config(&host, abi3_version, abi3t_version)?; Ok(interpreter_config) } @@ -1995,49 +2475,38 @@ mod tests { #[test] fn test_config_file_roundtrip() { - let config = InterpreterConfig { - abi3: true, - 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, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .pointer_width(Some(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() + .unwrap(); 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 config = InterpreterConfig { - abi3: false, - 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, - 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, + 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() + .unwrap(); + let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2046,20 +2515,18 @@ mod tests { #[test] fn test_config_file_roundtrip_with_escaping() { - let config = InterpreterConfig { - abi3: true, - 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, - }; + let implementation = PythonImplementation::CPython; + let version = MINIMUM_SUPPORTED_VERSION; + let config = InterpreterConfigBuilder::new(implementation, version) + .stable_abi(StableAbi::Abi3) + .unwrap() + .pointer_width(Some(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() + .unwrap(); let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2071,46 +2538,141 @@ 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("version=3.8".as_bytes()).unwrap(), - InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, - shared: true, - abi3: false, - 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, - } + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() ) } #[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("version=3.8\next_suffix=.python38.so".as_bytes()) .unwrap(), - InterpreterConfig { - version: PythonVersion { major: 3, minor: 8 }, - implementation: PythonImplementation::CPython, - shared: true, - abi3: false, - 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, - } + InterpreterConfigBuilder::new(implementation, version,) + .finalize() + .unwrap() + ) + } + + #[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; + // Legacy: build_flags=Py_GIL_DISABLED with no target_abi infers free-threaded. + assert_eq!( + InterpreterConfig::from_reader("version=3.13\nbuild_flags=Py_GIL_DISABLED".as_bytes()) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + .unwrap() + ); + // Canonical: target_abi=free_threaded. + assert_eq!( + InterpreterConfig::from_reader( + "version=3.13\ntarget_abi=CPython-free_threaded-3.13".as_bytes() + ) + .unwrap(), + InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .finalize() + .unwrap() + ); + // target_abi=gil_enabled with build_flags=Py_GIL_DISABLED is inconsistent and rejected. + assert!(InterpreterConfig::from_reader( + "version=3.13\ntarget_abi=CPython-gil_enabled-3.13\nbuild_flags=Py_GIL_DISABLED" + .as_bytes() + ) + .is_err()); + // build_flags=Py_GIL_DISABLED on a builder without target_abi is ok + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(InterpreterConfigBuilder::new(implementation, version) + .build_flags(flags) + .unwrap() + .finalize() + .unwrap() + .target_abi + .kind + .is_free_threaded()); + + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY312) + .build_flags(flags) + .unwrap() + .finalize() + .is_err() + ); + + let mut flags = BuildFlags::default(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY312) + .stable_abi(StableAbi::Abi3) + .unwrap() + .build_flags(flags) + .unwrap() + .finalize() + .is_err() + ); + + assert!( + InterpreterConfigBuilder::new(implementation, PythonVersion::PY38) + .free_threaded() + .is_err() + ); + } + + #[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() + .unwrap() + ); + } + + #[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] @@ -2197,22 +2759,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 { - abi3: false, - 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, - } + 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(Some(64)) + .finalize() + .unwrap() ); } @@ -2227,22 +2785,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 { - abi3: false, - 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, - } + 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(Some(64)) + .finalize() + .unwrap() ); sysconfigdata = Sysconfigdata::new(); @@ -2254,22 +2808,19 @@ 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 { - abi3: false, - 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, - } + 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(Some(64)) + .shared(false) + .finalize() + .unwrap() ); } @@ -2278,47 +2829,29 @@ 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 { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: 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) + .stable_abi(StableAbi::Abi3) + .unwrap() + .lib_name(Some("python3".into())) + .finalize() + .unwrap(); + 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 { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, - shared: true, - abi3: 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) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + .unwrap(); + assert_eq!(default_abi3_config(&host, min_version).unwrap(), config); } #[test] @@ -2337,23 +2870,14 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: false, - 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() + .unwrap(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2372,23 +2896,14 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: false, - 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() + .unwrap(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2407,23 +2922,14 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, - shared: true, - abi3: false, - 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() + .unwrap(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[test] @@ -2441,58 +2947,39 @@ mod tests { .unwrap() .unwrap(); - assert_eq!( - default_cross_compile(&cross_config).unwrap(), - InterpreterConfig { - implementation: PythonImplementation::PyPy, - version: PythonVersion { - major: 3, - minor: 11 - }, - shared: true, - abi3: false, - 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() + .unwrap(); + assert_eq!(default_cross_compile(&cross_config).unwrap(), config); } #[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) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize(), false, false, ) @@ -2501,34 +2988,32 @@ 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, - true, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize(), true, false, - false, ) .unwrap(), "python3", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - PyPy, - true, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY39) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize(), false, false, ) @@ -2537,13 +3022,10 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { - major: 3, - minor: 11 - }, - PyPy, - false, - false, + PythonAbiBuilder::new(PythonImplementation::PyPy, PythonVersion::PY311) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize(), false, false, ) @@ -2552,112 +3034,72 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 9 }, - CPython, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY310) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize(), false, true, - false, ) .unwrap(), - "python39_d", + "python3_d", ); // abi3 debug builds on windows use version-specific lib on 3.9 and older // 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) + .stable_abi(StableAbi::Abi3) + .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) + .stable_abi(StableAbi::Abi3) + .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, - 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 + 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, - false, + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY313) + .free_threaded() + .unwrap() + .finalize(), false, true, - true, ) .unwrap(), "python313t_d", @@ -2666,28 +3108,23 @@ mod tests { #[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", @@ -2695,12 +3132,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", @@ -2709,15 +3144,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", @@ -2725,12 +3154,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", @@ -2739,50 +3165,80 @@ 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) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize(), true, None, - false ) .unwrap(), "python3", ); } + #[test] + fn abi_builder_error_paths() { + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) + .free_threaded() + .unwrap() + .stable_abi(StableAbi::Abi3); + assert!(builder.is_err()); + assert!(builder + .unwrap_err() + .to_string() + .contains("ABI kind already set to")); + + let builder = PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY39) + .free_threaded(); + + 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 + ); + + assert!("invalid".parse::().is_err()); + assert!("CPython-invalid".parse::().is_err()); + assert!("CPython-free_threaded-invalid" + .parse::() + .is_err()); + + assert!( + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314) + .stable_abi(StableAbi::Abi3) + .unwrap() + .free_threaded() + .is_err() + ); + } + #[test] fn parse_cross_python_version() { let env_vars = CrossCompileEnvVars { @@ -2835,53 +3291,46 @@ mod tests { } #[test] - fn interpreter_version_reduced_to_abi3() { - let mut config = InterpreterConfig { - abi3: true, - 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 })) + 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) + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize(), + ) + .unwrap() + .finalize() .unwrap(); - assert_eq!(config.version, PythonVersion { major: 3, minor: 8 }); + assert_eq!(config.target_abi.version, target_version); + assert_eq!(config.version, host_version); } #[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, - }; + if !have_python_interpreter() { + return; + } - assert!(config - .fixup_for_abi3_version(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" - )); + 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" + )); + + let host_version = get_host_interpreter(None).unwrap().version; + if host_version >= PythonVersion::PY313 { + let interpreter = get_host_interpreter(Some(PythonVersion::PY313)); + assert_eq!( + interpreter.unwrap().target_abi.version, + PythonVersion::PY313 + ); + } } #[test] @@ -2908,7 +3357,7 @@ mod tests { version: Some(interpreter_config.version), implementation: Some(interpreter_config.implementation), target: triple!("x86_64-unknown-linux-gnu"), - abiflags: if interpreter_config.is_free_threaded() { + abiflags: if interpreter_config.target_abi.kind.is_free_threaded() { Some("t".into()) } else { None @@ -2923,23 +3372,16 @@ mod tests { let sysconfigdata = super::parse_sysconfigdata(sysconfigdata_path).unwrap(); let parsed_config = InterpreterConfig::from_sysconfigdata(&sysconfigdata).unwrap(); + assert_eq!(parsed_config.implementation, PythonImplementation::CPython); assert_eq!( - parsed_config, - InterpreterConfig { - abi3: false, - 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, - } - ) + parsed_config.target_abi.implementation, + PythonImplementation::CPython + ); + assert_eq!( + parsed_config.target_abi.kind.is_free_threaded(), + interpreter_config.target_abi.kind.is_free_threaded() + ); + assert_eq!(parsed_config.pointer_width, Some(64)); } #[test] @@ -3057,23 +3499,12 @@ mod tests { #[test] fn test_build_script_outputs_base() { - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 11, - }, - shared: true, - abi3: false, - 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() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3085,7 +3516,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + target_abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.target_abi + }, ..interpreter_config }; assert_eq!( @@ -3102,20 +3536,14 @@ mod tests { #[test] fn test_build_script_outputs_abi3() { - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 9 }, - shared: true, - abi3: 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) + .stable_abi(StableAbi::Abi3) + .unwrap() + .lib_name(Some("python3".into())) + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), @@ -3127,7 +3555,10 @@ mod tests { ); let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::PyPy, + target_abi: PythonAbi { + implementation: PythonImplementation::PyPy, + ..interpreter_config.target_abi + }, ..interpreter_config }; assert_eq!( @@ -3139,30 +3570,41 @@ mod tests { "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] ); - } - #[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 { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, + target_abi: PythonAbi { + implementation: PythonImplementation::CPython, + version: PythonVersion::PY315, + ..interpreter_config.target_abi }, - shared: true, - abi3: false, - 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, + ..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] + fn test_build_script_outputs_gil_disabled() { + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .free_threaded() + .unwrap() + .lib_name(Some("python3".into())) + .finalize() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3177,25 +3619,78 @@ 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).is_ok()); + + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + let config = builder + .stable_abi(StableAbi::Abi3) + .unwrap() + .build_flags(flags) + .unwrap() + .finalize() + .unwrap(); + // build flags win due to backward compatbility (abi3 feature is a no-op on ft builds) + assert!(config.target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + + // The reconciliation is order-independent: build_flags first, then stable_abi(Abi3) + // produces the same result as the previous ordering. + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + let config = builder + .build_flags(flags) + .unwrap() + .stable_abi(StableAbi::Abi3) + .unwrap() + .finalize() + .unwrap(); + assert!(config.target_abi.kind == PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded)); + + // Explicit GIL-enabled target with Py_GIL_DISABLED in build flags is contradictory and + // is rejected at finalize regardless of the order in which the setters were called. + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let target_abi = + PythonAbiBuilder::new(PythonImplementation::CPython, PythonVersion::PY314).finalize(); + let mut flags = BuildFlags::new(); + flags.0.insert(BuildFlag::Py_GIL_DISABLED); + assert!(builder + .target_abi(target_abi) + .unwrap() + .build_flags(flags) + .unwrap() + .finalize() + .is_err()); + + let builder = + InterpreterConfigBuilder::new(PythonImplementation::CPython, PythonVersion::PY314); + let config = builder.free_threaded().unwrap().finalize().unwrap(); + assert!(config.target_abi.kind.is_free_threaded()); + assert!(config.build_flags.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 { - implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, - shared: true, - abi3: false, - 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() + .unwrap(); assert_eq!( interpreter_config.build_script_outputs(), [ @@ -3236,20 +3731,11 @@ 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 }, - shared: true, - abi3: false, - 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() + .unwrap(); let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); @@ -3267,8 +3753,8 @@ mod tests { assert_eq!(config.lib_name, Some("python39".into())); // PyPy - config.implementation = PythonImplementation::PyPy; - config.version = PythonVersion { + config.target_abi.implementation = PythonImplementation::PyPy; + config.target_abi.version = PythonVersion { major: 3, minor: 11, }; @@ -3280,11 +3766,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.target_abi.implementation = PythonImplementation::CPython; // Free-threaded - config.build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); - config.version = PythonVersion { + config.target_abi.kind = PythonAbiKind::VersionSpecific(GilUsed::FreeThreaded); + config.target_abi.version = PythonVersion { major: 3, minor: 13, }; @@ -3303,7 +3789,10 @@ mod tests { config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); // abi3 - config.abi3 = true; + config.target_abi = PythonAbi { + kind: PythonAbiKind::Stable(StableAbi::Abi3), + ..config.target_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 9bc9a9c8cc9..23cbe1d7c62 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, PythonImplementation, PythonVersion, Triple, + CrossCompileConfig, GilUsed, InterpreterConfig, InterpreterConfigBuilder, PythonAbi, + PythonAbiBuilder, PythonAbiKind, PythonImplementation, PythonVersion, StableAbi, Triple, }; use target_lexicon::OperatingSystem; @@ -268,13 +269,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 +312,8 @@ 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, + StableAbi, }; pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. @@ -390,13 +392,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.target_abi.implementation { PythonImplementation::CPython => "Python", PythonImplementation::PyPy => "PyPy", PythonImplementation::GraalPy => "GraalPy", PythonImplementation::RustPython => "RustPython", }; - let version = &interpreter_config.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,26 +485,14 @@ mod tests { #[test] fn python_framework_link_args() { let mut buf = Vec::new(); - - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, - shared: true, - abi3: false, - 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: Some( + let implementation = PythonImplementation::CPython; + let version = PythonVersion::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .python_framework_prefix(Some( "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(), - ), - }; + )) + .finalize() + .unwrap(); // Does nothing on non-mac _add_python_framework_link_args( &interpreter_config, @@ -527,23 +517,11 @@ mod tests { #[test] #[cfg(feature = "resolve-config")] fn test_maximum_version_exceeded_formatting() { - let interpreter_config = InterpreterConfig { - implementation: PythonImplementation::CPython, - version: PythonVersion { - major: 3, - minor: 13, - }, - shared: true, - abi3: false, - 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::PY313; + let interpreter_config = InterpreterConfigBuilder::new(implementation, version) + .finalize() + .unwrap(); let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new( &interpreter_config, PythonVersion { diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index 7434f4dbc2b..82a90bfcfd3 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().target_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().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 @@ -170,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().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 7c9be0e4cb0..f3ea191ca1e 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, StableAbi, }, warn, PythonImplementation, }; @@ -45,32 +45,35 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Ok(()); } - match interpreter_config.implementation { + match interpreter_config.target_abi.implementation { PythonImplementation::CPython => { let versions = SUPPORTED_VERSIONS_CPYTHON; + let interp_version = interpreter_config.target_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.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.", + )); 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.target_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.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.version, + interpreter_config.target_abi.version, ); } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; ensure!( - interpreter_config.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.version, + interpreter_config.target_abi.version, versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.target_abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -111,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.target_abi.version >= versions.min, "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", - interpreter_config.version, + interpreter_config.target_abi.version, versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - if interpreter_config.version > versions.max { + if interpreter_config.target_abi.version > versions.max { let error = MaximumVersionExceeded::new(interpreter_config, versions.max); return Err(error.finish().into()); } @@ -125,21 +128,20 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::RustPython => {} } - if interpreter_config.abi3 { - 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." - ) + if let PythonAbiKind::Stable(abi) = interpreter_config.target_abi.kind { + match interpreter_config.target_abi.implementation { + PythonImplementation::CPython => match abi { + StableAbi::Abi3t => { + bail!("Abi3t builds are not yet supported") } - } + StableAbi::Abi3 => {} + }, 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 => {} }