Skip to content

Commit da71d84

Browse files
ngoldbaummessenseCopilot
authored
Refactor to store CPython ABI metadata in a struct combining two enums (#3110)
Towards #3064. This is purely refactoring, there should be no functional changes as a result of this. Currently the build metadata special-cases ABI3 builds or more generally assumes stable ABI builds and ABI3 builds are the same thing. With PEP 803 and the new abi3t ABI in Python 3.15, that is no longer the case. This replaces the old `ABI3Version` enum with a new struct combining two enums: ```rust /// 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, } /// 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, /// Stable ABI wheels will have a fixed user-specified minimum Python /// version Version(u8, u8), } /// 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, } ``` `StableAbiVersion` is just the old `Abi3Version` enum renamed since the concept of a minimum supported version is shared by abi3t. I have [a branch](main...ngoldbaum:maturin:abi3t) that adds an `Abi3t` variant for `StableAbiKind`. My goal with this PR is to make reviewing the subsequent PR adding abi3t support easier. Also see PyO3/pyo3#5924 where I made a similar change in PyO3. Here in Maturin I needed different types but in principle I could make the two implementations use shared code. I'm not sure if that's actually useful for anything in practice. --------- Co-authored-by: messense <messense@icloud.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent c1b6b77 commit da71d84

11 files changed

Lines changed: 245 additions & 128 deletions

File tree

src/binding_generator/pyo3_binding.rs

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use tracing::debug;
1616
use crate::BuildArtifact;
1717
use crate::BuildContext;
1818
use crate::PythonInterpreter;
19+
use crate::StableAbiKind;
1920
use crate::Target;
2021
use crate::archive_source::ArchiveSource;
2122
use crate::archive_source::GeneratedSourceData;
@@ -37,22 +38,26 @@ pub struct Pyo3BindingGenerator<'a> {
3738

3839
enum BindingType<'a> {
3940
Abi3(Option<&'a PythonInterpreter>),
40-
NonAbi3(&'a PythonInterpreter),
41+
VersionSpecific(&'a PythonInterpreter),
4142
}
4243

4344
impl<'a> Pyo3BindingGenerator<'a> {
4445
pub fn new(
45-
abi3: bool,
46+
stable_abi: Option<StableAbiKind>,
4647
interpreter: Option<&'a PythonInterpreter>,
4748
tempdir: Rc<TempDir>,
4849
) -> Result<Self> {
49-
let binding_type = match abi3 {
50-
true => BindingType::Abi3(interpreter),
51-
false => {
52-
let interpreter = interpreter.ok_or_else(|| anyhow!(
50+
let binding_type = match stable_abi {
51+
Some(kind) => match kind {
52+
StableAbiKind::Abi3 => BindingType::Abi3(interpreter),
53+
},
54+
None => {
55+
let interpreter = interpreter.ok_or_else(|| {
56+
anyhow!(
5357
"A python interpreter is required for non-abi3 builds but one was not provided"
54-
))?;
55-
BindingType::NonAbi3(interpreter)
58+
)
59+
})?;
60+
BindingType::VersionSpecific(interpreter)
5661
}
5762
};
5863
Ok(Self {
@@ -62,6 +67,29 @@ impl<'a> Pyo3BindingGenerator<'a> {
6267
}
6368
}
6469

70+
fn ext_suffix(
71+
target: &Target,
72+
interpreter: Option<&PythonInterpreter>,
73+
ext_name: &str,
74+
abi_name: &str,
75+
) -> String {
76+
if target.is_unix() {
77+
if target.is_cygwin() {
78+
format!("{ext_name}.{abi_name}.dll")
79+
} else {
80+
format!("{ext_name}.{abi_name}.so")
81+
}
82+
} else {
83+
match interpreter {
84+
Some(interpreter) if interpreter.is_windows_debug() => {
85+
format!("{ext_name}_d.pyd")
86+
}
87+
// Apparently there is no tag for abi3 on windows
88+
_ => format!("{ext_name}.pyd"),
89+
}
90+
}
91+
}
92+
6593
impl<'a> BindingGenerator for Pyo3BindingGenerator<'a> {
6694
fn generate_bindings(
6795
&mut self,
@@ -73,24 +101,8 @@ impl<'a> BindingGenerator for Pyo3BindingGenerator<'a> {
73101
let target = &context.project.target;
74102

75103
let so_filename = match self.binding_type {
76-
BindingType::Abi3(interpreter) => {
77-
if target.is_unix() {
78-
if target.is_cygwin() {
79-
format!("{ext_name}.abi3.dll")
80-
} else {
81-
format!("{ext_name}.abi3.so")
82-
}
83-
} else {
84-
match interpreter {
85-
Some(interpreter) if interpreter.is_windows_debug() => {
86-
format!("{ext_name}_d.pyd")
87-
}
88-
// Apparently there is no tag for abi3 on windows
89-
_ => format!("{ext_name}.pyd"),
90-
}
91-
}
92-
}
93-
BindingType::NonAbi3(interpreter) => interpreter.get_library_name(ext_name),
104+
BindingType::Abi3(interpreter) => ext_suffix(target, interpreter, ext_name, "abi3"),
105+
BindingType::VersionSpecific(interpreter) => interpreter.get_library_name(ext_name),
94106
};
95107
let artifact_target = ArtifactTarget::ExtensionModule(module.join(so_filename));
96108

src/bridge/detection.rs

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
//! to determine which binding model (pyo3, cffi, uniffi, bin) to use,
55
//! whether abi3 is enabled, and whether `generate-import-lib` is active.
66
7-
use super::{Abi3Version, BridgeModel, PyO3, PyO3Crate, PyO3MetadataRaw};
7+
use super::{
8+
BridgeModel, PyO3, PyO3Crate, PyO3MetadataRaw, StableAbi, StableAbiKind, StableAbiVersion,
9+
};
810
use crate::PyProjectToml;
911
use crate::pyproject_toml::FeatureSpec;
1012
use anyhow::{Context, Result, bail};
@@ -122,13 +124,14 @@ pub fn find_bridge(
122124
}
123125
}
124126

125-
return if let Some(abi3_version) = has_abi3(&deps, &extra_pyo3_features)? {
126-
eprintln!("🔗 Found {lib} bindings with abi3 support");
127+
return if let Some(stable_abi) = has_stable_abi(&deps, &extra_pyo3_features)? {
128+
let kind = stable_abi.kind;
129+
eprintln!("🔗 Found {lib} bindings with {kind} support");
127130
let pyo3 = bridge.pyo3().expect("should be pyo3 bindings");
128131
let bindings = PyO3 {
129132
crate_name: lib,
130133
version: pyo3.version.clone(),
131-
abi3: Some(abi3_version),
134+
stable_abi: Some(stable_abi),
132135
metadata: pyo3.metadata.clone(),
133136
};
134137
Ok(BridgeModel::PyO3(bindings))
@@ -173,42 +176,69 @@ pub fn is_generating_import_lib(cargo_metadata: &Metadata) -> Result<bool> {
173176
Ok(false)
174177
}
175178

176-
/// pyo3 supports building abi3 wheels if the unstable-api feature is not selected
177-
fn has_abi3(
179+
fn has_stable_abi(
178180
deps: &HashMap<&str, &Node>,
179181
extra_features: &HashMap<&str, Vec<String>>,
180-
) -> Result<Option<Abi3Version>> {
182+
) -> Result<Option<StableAbi>> {
183+
let abi3 = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3)?;
184+
if abi3.is_some() {
185+
return Ok(abi3);
186+
}
187+
Ok(None)
188+
}
189+
190+
/// pyo3 supports building stable abi wheels if the unstable-api feature is not selected
191+
fn has_stable_abi_from_kind(
192+
deps: &HashMap<&str, &Node>,
193+
extra_features: &HashMap<&str, Vec<String>>,
194+
abi_kind: StableAbiKind,
195+
) -> Result<Option<StableAbi>> {
181196
for &lib in PYO3_BINDING_CRATES.iter() {
182197
let lib = lib.as_str();
183198
if let Some(&pyo3_crate) = deps.get(lib) {
184199
let extra = extra_features.get(lib);
185200
// Find the minimal abi3 python version. If there is none, abi3 hasn't been selected
186-
// This parser abi3-py{major}{minor} and returns the minimal (major, minor) tuple
201+
// Find the minimal stable abi python version. If there is none, stable abi hasn't been selected
202+
// This parses abi3-py{major}{minor} and returns the minimal (major, minor) tuple
187203
let all_features: Vec<&str> = pyo3_crate
188204
.features
189205
.iter()
190206
.map(AsRef::as_ref)
191207
.chain(extra.into_iter().flatten().map(String::as_str))
192208
.collect();
193209

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

196-
let min_abi3_version = all_features
216+
let min_stable_abi_version = all_features
197217
.iter()
198-
.filter(|&&x| x.starts_with("abi3-py") && x.len() >= "abi3-pyxx".len())
218+
.filter(|&&x| x.starts_with(search_str.as_str()) && x.len() >= filter_len)
199219
.map(|x| {
200220
Ok((
201-
(x.as_bytes()[7] as char).to_string().parse::<u8>()?,
202-
x[8..].parse::<u8>()?,
221+
(x.as_bytes()[offset] as char).to_string().parse::<u8>()?,
222+
x[offset + 1..].parse::<u8>()?,
203223
))
204224
})
205225
.collect::<Result<Vec<(u8, u8)>>>()
206226
.context(format!("Bogus {lib} cargo features"))?
207227
.into_iter()
208228
.min();
209-
match min_abi3_version {
210-
Some((major, minor)) => return Ok(Some(Abi3Version::Version(major, minor))),
211-
None if abi3_selected => return Ok(Some(Abi3Version::CurrentPython)),
229+
match min_stable_abi_version {
230+
Some((major, minor)) => {
231+
return Ok(Some(StableAbi {
232+
kind: abi_kind,
233+
version: StableAbiVersion::Version(major, minor),
234+
}));
235+
}
236+
None if stable_abi_selected => {
237+
return Ok(Some(StableAbi {
238+
kind: abi_kind,
239+
version: StableAbiVersion::CurrentPython,
240+
}));
241+
}
212242
None => {}
213243
}
214244
}
@@ -238,7 +268,7 @@ fn find_pyo3_bindings(
238268
Ok(Some(PyO3 {
239269
crate_name: PyO3Crate::PyO3,
240270
version,
241-
abi3: None,
271+
stable_abi: None,
242272
metadata,
243273
}))
244274
} else if deps.get("pyo3-ffi").is_some() {
@@ -252,7 +282,7 @@ fn find_pyo3_bindings(
252282
Ok(Some(PyO3 {
253283
crate_name: PyO3Crate::PyO3Ffi,
254284
version,
255-
abi3: None,
285+
stable_abi: None,
256286
metadata,
257287
}))
258288
} else {

src/bridge/mod.rs

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,70 @@ impl TryFrom<PyO3MetadataRaw> for PyO3Metadata {
114114
}
115115
}
116116

117-
/// Python version to use as the abi3 target.
118-
#[derive(Clone, Debug, PartialEq, Eq)]
119-
pub enum Abi3Version {
120-
/// abi3 wheels will have a minimum Python version matching the version of
121-
/// the current Python interpreter
117+
/// struct describing ABI layout to use for build
118+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119+
pub struct StableAbi {
120+
/// The "kind" of stable ABI. Either abi3 or abi3t currently.
121+
pub kind: StableAbiKind,
122+
/// The minimum Python version to build for.
123+
pub version: StableAbiVersion,
124+
}
125+
126+
impl StableAbi {
127+
/// Create a StableAbi instance from a known abi3 version
128+
pub fn from_abi3_version(major: u8, minor: u8) -> StableAbi {
129+
StableAbi {
130+
kind: StableAbiKind::Abi3,
131+
version: StableAbiVersion::Version(major, minor),
132+
}
133+
}
134+
}
135+
136+
/// Python version to use as the abi3/abi3t target.
137+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
138+
pub enum StableAbiVersion {
139+
/// Stable ABI wheels will have a minimum Python version matching the
140+
/// version of the current Python interpreter
122141
CurrentPython,
123-
/// abi3 wheels will have a fixed minimum Python version
142+
/// Stable ABI wheels will have a fixed user-specified minimum Python
143+
/// version
124144
Version(u8, u8),
125145
}
126146

147+
impl StableAbiVersion {
148+
/// Convert `StableAbiVersion` into an Option, where CurrentPython maps None
149+
pub fn min_version(&self) -> Option<(u8, u8)> {
150+
match self {
151+
StableAbiVersion::CurrentPython => None,
152+
StableAbiVersion::Version(major, minor) => Some((*major, *minor)),
153+
}
154+
}
155+
}
156+
157+
/// The "kind" of stable ABI. Either abi3 or abi3t currently.
158+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159+
pub enum StableAbiKind {
160+
/// The original stable ABI, supporting Python 3.2 and up
161+
Abi3,
162+
}
163+
164+
impl fmt::Display for StableAbiKind {
165+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166+
match self {
167+
StableAbiKind::Abi3 => write!(f, "abi3"),
168+
}
169+
}
170+
}
171+
172+
impl StableAbiKind {
173+
/// The tag to use for wheel building
174+
pub fn wheel_tag(&self) -> &str {
175+
match self {
176+
StableAbiKind::Abi3 => "abi3",
177+
}
178+
}
179+
}
180+
127181
/// The name and version of the pyo3 bindings crate
128182
#[derive(Clone, Debug, PartialEq, Eq)]
129183
pub struct PyO3 {
@@ -132,7 +186,7 @@ pub struct PyO3 {
132186
/// pyo3 bindings crate version
133187
pub version: semver::Version,
134188
/// abi3 support
135-
pub abi3: Option<Abi3Version>,
189+
pub stable_abi: Option<StableAbi>,
136190
/// pyo3 metadata
137191
pub metadata: Option<PyO3Metadata>,
138192
}
@@ -150,8 +204,12 @@ impl PyO3 {
150204
} else {
151205
MINIMUM_PYTHON_MINOR
152206
};
153-
if let Some(Abi3Version::Version(_, abi3_minor)) = self.abi3.as_ref() {
154-
min_minor.max(*abi3_minor as usize)
207+
if let Some(stable_abi) = self.stable_abi.as_ref() {
208+
if let StableAbiVersion::Version(_, abi3_minor) = stable_abi.version {
209+
min_minor.max(abi3_minor as usize)
210+
} else {
211+
min_minor
212+
}
155213
} else {
156214
min_minor
157215
}
@@ -279,10 +337,14 @@ impl BridgeModel {
279337

280338
/// Is using abi3
281339
pub fn is_abi3(&self) -> bool {
282-
match self.pyo3() {
283-
Some(pyo3) => pyo3.abi3.is_some(),
284-
None => false,
285-
}
340+
self.pyo3()
341+
.and_then(|pyo3| match pyo3.stable_abi {
342+
Some(stable_abi) => match stable_abi.kind {
343+
StableAbiKind::Abi3 => Some(true),
344+
},
345+
None => None,
346+
})
347+
.is_some_and(|x| x)
286348
}
287349

288350
/// free-threaded Python support

src/build_options.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ impl BuildOptions {
155155
#[cfg(test)]
156156
mod tests {
157157
use super::*;
158-
use crate::bridge::{Abi3Version, PyO3, PyO3Crate, find_bridge};
158+
use crate::bridge::{PyO3, PyO3Crate, StableAbi, find_bridge};
159159
use crate::python_interpreter::InterpreterResolver;
160160
use crate::test_utils::test_crate_path;
161161
use crate::{BridgeModel, Target};
@@ -192,7 +192,7 @@ mod tests {
192192
let bridge = BridgeModel::PyO3(PyO3 {
193193
crate_name: PyO3Crate::PyO3,
194194
version: semver::Version::new(0, 28, 2),
195-
abi3: Some(Abi3Version::Version(3, 7)),
195+
stable_abi: Some(StableAbi::from_abi3_version(3, 7)),
196196
metadata: Some(PyO3Metadata {
197197
cpython: PyO3VersionMetadata {
198198
min_minor: 7,

0 commit comments

Comments
 (0)