Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 38 additions & 26 deletions src/binding_generator/pyo3_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use tracing::debug;
use crate::BuildArtifact;
use crate::BuildContext;
use crate::PythonInterpreter;
use crate::StableAbiKind;
use crate::Target;
use crate::archive_source::ArchiveSource;
use crate::archive_source::GeneratedSourceData;
Expand All @@ -37,22 +38,26 @@ pub struct Pyo3BindingGenerator<'a> {

enum BindingType<'a> {
Abi3(Option<&'a PythonInterpreter>),
NonAbi3(&'a PythonInterpreter),
VersionSpecific(&'a PythonInterpreter),
}

impl<'a> Pyo3BindingGenerator<'a> {
pub fn new(
abi3: bool,
stable_abi: Option<StableAbiKind>,
interpreter: Option<&'a PythonInterpreter>,
tempdir: Rc<TempDir>,
) -> Result<Self> {
let binding_type = match abi3 {
true => BindingType::Abi3(interpreter),
false => {
let interpreter = interpreter.ok_or_else(|| anyhow!(
let binding_type = match stable_abi {
Some(kind) => match kind {
StableAbiKind::Abi3 => BindingType::Abi3(interpreter),
},
None => {
let interpreter = interpreter.ok_or_else(|| {
anyhow!(
"A python interpreter is required for non-abi3 builds but one was not provided"
))?;
BindingType::NonAbi3(interpreter)
)
})?;
BindingType::VersionSpecific(interpreter)
}
};
Ok(Self {
Expand All @@ -62,6 +67,29 @@ impl<'a> Pyo3BindingGenerator<'a> {
}
}

fn ext_suffix(
target: &Target,
interpreter: Option<&PythonInterpreter>,
ext_name: &str,
abi_name: &str,
) -> String {
if target.is_unix() {
if target.is_cygwin() {
format!("{ext_name}.{abi_name}.dll")
} else {
format!("{ext_name}.{abi_name}.so")
}
} else {
match interpreter {
Some(interpreter) if interpreter.is_windows_debug() => {
format!("{ext_name}_d.pyd")
}
// Apparently there is no tag for abi3 on windows
_ => format!("{ext_name}.pyd"),
}
}
}

impl<'a> BindingGenerator for Pyo3BindingGenerator<'a> {
fn generate_bindings(
&mut self,
Expand All @@ -73,24 +101,8 @@ impl<'a> BindingGenerator for Pyo3BindingGenerator<'a> {
let target = &context.project.target;

let so_filename = match self.binding_type {
BindingType::Abi3(interpreter) => {
if target.is_unix() {
if target.is_cygwin() {
format!("{ext_name}.abi3.dll")
} else {
format!("{ext_name}.abi3.so")
}
} else {
match interpreter {
Some(interpreter) if interpreter.is_windows_debug() => {
format!("{ext_name}_d.pyd")
}
// Apparently there is no tag for abi3 on windows
_ => format!("{ext_name}.pyd"),
}
}
}
BindingType::NonAbi3(interpreter) => interpreter.get_library_name(ext_name),
BindingType::Abi3(interpreter) => ext_suffix(target, interpreter, ext_name, "abi3"),
BindingType::VersionSpecific(interpreter) => interpreter.get_library_name(ext_name),
};
let artifact_target = ArtifactTarget::ExtensionModule(module.join(so_filename));

Expand Down
66 changes: 48 additions & 18 deletions src/bridge/detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
//! to determine which binding model (pyo3, cffi, uniffi, bin) to use,
//! whether abi3 is enabled, and whether `generate-import-lib` is active.

use super::{Abi3Version, BridgeModel, PyO3, PyO3Crate, PyO3MetadataRaw};
use super::{
BridgeModel, PyO3, PyO3Crate, PyO3MetadataRaw, StableAbi, StableAbiKind, StableAbiVersion,
};
use crate::PyProjectToml;
use crate::pyproject_toml::FeatureSpec;
use anyhow::{Context, Result, bail};
Expand Down Expand Up @@ -122,13 +124,14 @@ pub fn find_bridge(
}
}

return if let Some(abi3_version) = has_abi3(&deps, &extra_pyo3_features)? {
eprintln!("🔗 Found {lib} bindings with abi3 support");
return if let Some(stable_abi) = has_stable_abi(&deps, &extra_pyo3_features)? {
let kind = stable_abi.kind;
eprintln!("🔗 Found {lib} bindings with {kind} support");
let pyo3 = bridge.pyo3().expect("should be pyo3 bindings");
let bindings = PyO3 {
crate_name: lib,
version: pyo3.version.clone(),
abi3: Some(abi3_version),
stable_abi: Some(stable_abi),
metadata: pyo3.metadata.clone(),
};
Ok(BridgeModel::PyO3(bindings))
Expand Down Expand Up @@ -173,42 +176,69 @@ pub fn is_generating_import_lib(cargo_metadata: &Metadata) -> Result<bool> {
Ok(false)
}

/// pyo3 supports building abi3 wheels if the unstable-api feature is not selected
fn has_abi3(
fn has_stable_abi(
deps: &HashMap<&str, &Node>,
extra_features: &HashMap<&str, Vec<String>>,
) -> Result<Option<Abi3Version>> {
) -> Result<Option<StableAbi>> {
let abi3 = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3)?;
if abi3.is_some() {
return Ok(abi3);
}
Ok(None)
}

/// pyo3 supports building stable abi wheels if the unstable-api feature is not selected
fn has_stable_abi_from_kind(
deps: &HashMap<&str, &Node>,
extra_features: &HashMap<&str, Vec<String>>,
abi_kind: StableAbiKind,
) -> Result<Option<StableAbi>> {
for &lib in PYO3_BINDING_CRATES.iter() {
let lib = lib.as_str();
if let Some(&pyo3_crate) = deps.get(lib) {
let extra = extra_features.get(lib);
// Find the minimal abi3 python version. If there is none, abi3 hasn't been selected
// This parser abi3-py{major}{minor} and returns the minimal (major, minor) tuple
// Find the minimal stable abi python version. If there is none, stable abi hasn't been selected
// This parses abi3-py{major}{minor} and returns the minimal (major, minor) tuple
let all_features: Vec<&str> = pyo3_crate
.features
.iter()
.map(AsRef::as_ref)
.chain(extra.into_iter().flatten().map(String::as_str))
.collect();

let abi3_selected = all_features.contains(&"abi3");
let abi_str = format!("{abi_kind}");
let search_str = format!("{abi_kind}-py");
let stable_abi_selected = all_features.contains(&abi_str.as_str());
let offset = search_str.len();
let filter_len = offset + 2;

let min_abi3_version = all_features
let min_stable_abi_version = all_features
.iter()
.filter(|&&x| x.starts_with("abi3-py") && x.len() >= "abi3-pyxx".len())
.filter(|&&x| x.starts_with(search_str.as_str()) && x.len() >= filter_len)
.map(|x| {
Ok((
(x.as_bytes()[7] as char).to_string().parse::<u8>()?,
x[8..].parse::<u8>()?,
(x.as_bytes()[offset] as char).to_string().parse::<u8>()?,
x[offset + 1..].parse::<u8>()?,
))
})
.collect::<Result<Vec<(u8, u8)>>>()
.context(format!("Bogus {lib} cargo features"))?
.into_iter()
.min();
match min_abi3_version {
Some((major, minor)) => return Ok(Some(Abi3Version::Version(major, minor))),
None if abi3_selected => return Ok(Some(Abi3Version::CurrentPython)),
match min_stable_abi_version {
Some((major, minor)) => {
return Ok(Some(StableAbi {
kind: abi_kind,
version: StableAbiVersion::Version(major, minor),
}));
}
None if stable_abi_selected => {
return Ok(Some(StableAbi {
kind: abi_kind,
version: StableAbiVersion::CurrentPython,
}));
}
None => {}
}
}
Expand Down Expand Up @@ -238,7 +268,7 @@ fn find_pyo3_bindings(
Ok(Some(PyO3 {
crate_name: PyO3Crate::PyO3,
version,
abi3: None,
stable_abi: None,
metadata,
}))
} else if deps.get("pyo3-ffi").is_some() {
Expand All @@ -252,7 +282,7 @@ fn find_pyo3_bindings(
Ok(Some(PyO3 {
crate_name: PyO3Crate::PyO3Ffi,
version,
abi3: None,
stable_abi: None,
metadata,
}))
} else {
Expand Down
88 changes: 75 additions & 13 deletions src/bridge/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,70 @@ impl TryFrom<PyO3MetadataRaw> for PyO3Metadata {
}
}

/// Python version to use as the abi3 target.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Abi3Version {
/// abi3 wheels will have a minimum Python version matching the version of
/// the current Python interpreter
/// struct describing ABI layout to use for build
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StableAbi {
/// The "kind" of stable ABI. Either abi3 or abi3t currently.
pub kind: StableAbiKind,
/// The minimum Python version to build for.
pub version: StableAbiVersion,
}

impl StableAbi {
/// Create a StableAbi instance from a known abi3 version
pub fn from_abi3_version(major: u8, minor: u8) -> StableAbi {
StableAbi {
kind: StableAbiKind::Abi3,
version: StableAbiVersion::Version(major, minor),
}
}
}

/// Python version to use as the abi3/abi3t target.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StableAbiVersion {
/// Stable ABI wheels will have a minimum Python version matching the
/// version of the current Python interpreter
CurrentPython,
/// abi3 wheels will have a fixed minimum Python version
/// Stable ABI wheels will have a fixed user-specified minimum Python
/// version
Version(u8, u8),
}

impl StableAbiVersion {
/// Convert `StableAbiVersion` into an Option, where CurrentPython maps None
pub fn min_version(&self) -> Option<(u8, u8)> {
match self {
StableAbiVersion::CurrentPython => None,
StableAbiVersion::Version(major, minor) => Some((*major, *minor)),
}
}
}

/// The "kind" of stable ABI. Either abi3 or abi3t currently.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StableAbiKind {
/// The original stable ABI, supporting Python 3.2 and up
Abi3,
}

impl fmt::Display for StableAbiKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StableAbiKind::Abi3 => write!(f, "abi3"),
}
}
}

impl StableAbiKind {
/// The tag to use for wheel building
pub fn wheel_tag(&self) -> &str {
match self {
StableAbiKind::Abi3 => "abi3",
}
}
}

/// The name and version of the pyo3 bindings crate
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PyO3 {
Expand All @@ -132,7 +186,7 @@ pub struct PyO3 {
/// pyo3 bindings crate version
pub version: semver::Version,
/// abi3 support
pub abi3: Option<Abi3Version>,
pub stable_abi: Option<StableAbi>,
/// pyo3 metadata
pub metadata: Option<PyO3Metadata>,
}
Expand All @@ -150,8 +204,12 @@ impl PyO3 {
} else {
MINIMUM_PYTHON_MINOR
};
if let Some(Abi3Version::Version(_, abi3_minor)) = self.abi3.as_ref() {
min_minor.max(*abi3_minor as usize)
if let Some(stable_abi) = self.stable_abi.as_ref() {
if let StableAbiVersion::Version(_, abi3_minor) = stable_abi.version {
min_minor.max(abi3_minor as usize)
} else {
min_minor
}
} else {
min_minor
}
Expand Down Expand Up @@ -279,10 +337,14 @@ impl BridgeModel {

/// Is using abi3
pub fn is_abi3(&self) -> bool {
match self.pyo3() {
Some(pyo3) => pyo3.abi3.is_some(),
None => false,
}
self.pyo3()
.and_then(|pyo3| match pyo3.stable_abi {
Some(stable_abi) => match stable_abi.kind {
StableAbiKind::Abi3 => Some(true),
},
None => None,
})
.is_some_and(|x| x)
}

/// free-threaded Python support
Expand Down
4 changes: 2 additions & 2 deletions src/build_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl BuildOptions {
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::{Abi3Version, PyO3, PyO3Crate, find_bridge};
use crate::bridge::{PyO3, PyO3Crate, StableAbi, find_bridge};
use crate::python_interpreter::InterpreterResolver;
use crate::test_utils::test_crate_path;
use crate::{BridgeModel, Target};
Expand Down Expand Up @@ -192,7 +192,7 @@ mod tests {
let bridge = BridgeModel::PyO3(PyO3 {
crate_name: PyO3Crate::PyO3,
version: semver::Version::new(0, 28, 2),
abi3: Some(Abi3Version::Version(3, 7)),
stable_abi: Some(StableAbi::from_abi3_version(3, 7)),
metadata: Some(PyO3Metadata {
cpython: PyO3VersionMetadata {
min_minor: 7,
Expand Down
Loading
Loading