mod detection;
pub use detection::{
CrateDependencies, find_bridge, find_bridge_with_deps, has_windows_import_lib_support,
upgrade_bridge_stable_abi,
};
use std::{fmt, str::FromStr};
use anyhow::Context;
use serde::{Deserialize, Serialize};
use crate::python_interpreter::{
MAXIMUM_PYPY_MINOR, MAXIMUM_PYTHON_MINOR, MINIMUM_PYPY_MINOR, MINIMUM_PYTHON_MINOR,
PythonInterpreter,
};
pub const ABI3T_MINIMUM_PYTHON_MINOR: u8 = 15;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PyO3Crate {
PyO3,
PyO3Ffi,
}
impl PyO3Crate {
pub fn as_str(&self) -> &str {
match self {
PyO3Crate::PyO3 => "pyo3",
PyO3Crate::PyO3Ffi => "pyo3-ffi",
}
}
}
impl fmt::Debug for PyO3Crate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl fmt::Display for PyO3Crate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for PyO3Crate {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"pyo3" => Ok(PyO3Crate::PyO3),
"pyo3-ffi" => Ok(PyO3Crate::PyO3Ffi),
_ => anyhow::bail!("unknown binding crate: {}", s),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bindings {
PyO3,
PyO3Ffi,
Cffi,
UniFfi,
Bin,
}
impl Bindings {
pub const ALL: [Bindings; 5] = [
Bindings::PyO3,
Bindings::PyO3Ffi,
Bindings::Cffi,
Bindings::UniFfi,
Bindings::Bin,
];
pub const VARIANTS: [&'static str; 5] = [
Bindings::PyO3.as_str(),
Bindings::PyO3Ffi.as_str(),
Bindings::Cffi.as_str(),
Bindings::UniFfi.as_str(),
Bindings::Bin.as_str(),
];
pub const fn as_str(self) -> &'static str {
match self {
Bindings::PyO3 => "pyo3",
Bindings::PyO3Ffi => "pyo3-ffi",
Bindings::Cffi => "cffi",
Bindings::UniFfi => "uniffi",
Bindings::Bin => "bin",
}
}
pub const fn description(self) -> &'static str {
match self {
Bindings::PyO3 => "PyO3 bindings",
Bindings::PyO3Ffi => "pyo3-ffi (raw FFI) bindings",
Bindings::Cffi => "CFFI bindings",
Bindings::UniFfi => "UniFFI bindings",
Bindings::Bin => "Rust binary",
}
}
}
impl fmt::Display for Bindings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for Bindings {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Bindings::ALL
.into_iter()
.find(|binding| binding.as_str() == s)
.with_context(|| {
format!(
"unknown bindings type `{s}`, expected one of {}",
Bindings::VARIANTS.join(", ")
)
})
}
}
impl Serialize for Bindings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Bindings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct BindingsVisitor;
impl serde::de::Visitor<'_> for BindingsVisitor {
type Value = Bindings;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a bindings type")
}
fn visit_str<E>(self, value: &str) -> Result<Bindings, E>
where
E: serde::de::Error,
{
Bindings::ALL
.into_iter()
.find(|binding| binding.as_str() == value)
.ok_or_else(|| E::unknown_variant(value, &Bindings::VARIANTS))
}
}
deserializer.deserialize_str(BindingsVisitor)
}
}
impl clap::ValueEnum for Bindings {
fn value_variants<'a>() -> &'a [Self] {
&Bindings::ALL
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(clap::builder::PossibleValue::new(self.as_str()))
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Bindings {
fn schema_name() -> std::borrow::Cow<'static, str> {
"Bindings".into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
"maturin::Bindings".into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let one_of: Vec<serde_json::Value> = Bindings::ALL
.iter()
.map(|binding| {
serde_json::json!({
"description": binding.description(),
"type": "string",
"const": binding.as_str(),
})
})
.collect();
schemars::json_schema!({
"description": "The kind of bindings to use.",
"oneOf": one_of,
})
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct PyO3VersionMetadataRaw {
#[serde(rename = "min-version")]
pub min_version: String,
#[serde(rename = "max-version")]
pub max_version: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PyO3MetadataRaw {
pub cpython: PyO3VersionMetadataRaw,
pub pypy: PyO3VersionMetadataRaw,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PyO3VersionMetadata {
pub min_minor: usize,
pub max_minor: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PyO3Metadata {
pub cpython: PyO3VersionMetadata,
pub pypy: PyO3VersionMetadata,
}
impl TryFrom<PyO3VersionMetadataRaw> for PyO3VersionMetadata {
type Error = anyhow::Error;
fn try_from(raw: PyO3VersionMetadataRaw) -> Result<Self, Self::Error> {
let min_version = raw
.min_version
.rsplit('.')
.next()
.context("invalid min-version in pyo3-ffi metadata")?
.parse()?;
let max_version = raw
.max_version
.rsplit('.')
.next()
.context("invalid max-version in pyo3-ffi metadata")?
.parse()?;
Ok(Self {
min_minor: min_version,
max_minor: max_version,
})
}
}
impl TryFrom<PyO3MetadataRaw> for PyO3Metadata {
type Error = anyhow::Error;
fn try_from(raw: PyO3MetadataRaw) -> Result<Self, Self::Error> {
Ok(Self {
cpython: PyO3VersionMetadata::try_from(raw.cpython)?,
pypy: PyO3VersionMetadata::try_from(raw.pypy)?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StableAbi {
pub kind: StableAbiKind,
pub version: StableAbiVersion,
}
impl StableAbi {
pub fn from_abi3_version(major: u8, minor: u8) -> StableAbi {
StableAbi {
kind: StableAbiKind::Abi3,
version: StableAbiVersion::Version(major, minor),
}
}
pub fn from_abi3t_version(major: u8, minor: u8) -> StableAbi {
StableAbi {
kind: StableAbiKind::Abi3t,
version: StableAbiVersion::Version(major, minor),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StableAbiVersion {
CurrentPython,
Version(u8, u8),
}
impl StableAbiVersion {
pub fn min_version(&self) -> Option<(u8, u8)> {
match self {
StableAbiVersion::CurrentPython => None,
StableAbiVersion::Version(major, minor) => Some((*major, *minor)),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StableAbiKind {
Abi3,
Abi3t,
}
impl fmt::Display for StableAbiKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StableAbiKind::Abi3 => write!(f, "abi3"),
StableAbiKind::Abi3t => write!(f, "abi3t"),
}
}
}
impl StableAbiKind {
pub fn wheel_tag(&self) -> &str {
match self {
StableAbiKind::Abi3 => "abi3",
StableAbiKind::Abi3t => "abi3.abi3t",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PyO3 {
pub crate_name: PyO3Crate,
pub version: semver::Version,
pub stable_abi: Option<StableAbi>,
pub metadata: Option<PyO3Metadata>,
}
impl PyO3 {
fn minimal_python_minor_version(&self) -> usize {
let major_version = self.version.major;
let minor_version = self.version.minor;
let min_minor = if let Some(metadata) = self.metadata.as_ref() {
metadata.cpython.min_minor
} else if (major_version, minor_version) >= (0, 16) {
7
} else {
MINIMUM_PYTHON_MINOR
};
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
}
}
fn maximum_python_minor_version(&self) -> usize {
if let Some(metadata) = self.metadata.as_ref() {
metadata.cpython.max_minor
} else {
MAXIMUM_PYTHON_MINOR
}
}
fn minimal_pypy_minor_version(&self) -> usize {
let major_version = self.version.major;
let minor_version = self.version.minor;
if let Some(metadata) = self.metadata.as_ref() {
metadata.pypy.min_minor
} else if (major_version, minor_version) >= (0, 23) {
9
} else if (major_version, minor_version) >= (0, 14) {
7
} else {
MINIMUM_PYPY_MINOR
}
}
fn maximum_pypy_minor_version(&self) -> usize {
if let Some(metadata) = self.metadata.as_ref() {
metadata.pypy.max_minor
} else {
MAXIMUM_PYPY_MINOR
}
}
fn supports_free_threaded(&self) -> bool {
let major_version = self.version.major;
let minor_version = self.version.minor;
(major_version, minor_version) >= (0, 23)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BridgeModel {
Bin(Option<PyO3>),
PyO3(PyO3),
Cffi,
UniFfi,
}
impl BridgeModel {
pub fn pyo3(&self) -> Option<&PyO3> {
match self {
BridgeModel::Bin(Some(bindings)) => Some(bindings),
BridgeModel::PyO3(bindings) => Some(bindings),
_ => None,
}
}
pub fn is_pyo3(&self) -> bool {
matches!(self, BridgeModel::PyO3(_) | BridgeModel::Bin(Some(_)))
}
pub fn is_pyo3_crate(&self, name: PyO3Crate) -> bool {
match self {
BridgeModel::Bin(Some(bindings)) => bindings.crate_name == name,
BridgeModel::PyO3(bindings) => bindings.crate_name == name,
_ => false,
}
}
pub fn is_bin(&self) -> bool {
matches!(self, BridgeModel::Bin(_))
}
pub fn minimal_python_minor_version(&self) -> usize {
match self.pyo3() {
Some(bindings) => bindings.minimal_python_minor_version(),
None => MINIMUM_PYTHON_MINOR,
}
}
pub fn maximum_python_minor_version(&self) -> usize {
match self.pyo3() {
Some(bindings) => bindings.maximum_python_minor_version(),
None => MAXIMUM_PYTHON_MINOR,
}
}
pub fn minimal_pypy_minor_version(&self) -> usize {
match self.pyo3() {
Some(bindings) => bindings.minimal_pypy_minor_version(),
None => MINIMUM_PYPY_MINOR,
}
}
pub fn maximum_pypy_minor_version(&self) -> usize {
match self.pyo3() {
Some(bindings) => bindings.maximum_pypy_minor_version(),
None => MAXIMUM_PYPY_MINOR,
}
}
pub fn has_stable_abi(&self) -> bool {
self.pyo3()
.and_then(|pyo3| pyo3.stable_abi.as_ref())
.is_some()
}
pub fn is_stable_abi_for_interpreter(&self, interpreter: &PythonInterpreter) -> bool {
self.stable_abi_for_interpreter(interpreter).is_some()
}
pub fn stable_abi_for_interpreter(&self, interpreter: &PythonInterpreter) -> Option<StableAbi> {
self.pyo3()?.stable_abi.filter(|stable_abi| {
interpreter.has_stable_api(stable_abi.kind)
&& stable_abi
.version
.min_version()
.is_none_or(|(major, minor)| {
(interpreter.major as u8, interpreter.minor as u8) >= (major, minor)
})
})
}
pub fn supports_free_threaded(&self) -> bool {
match self {
BridgeModel::Bin(Some(bindings)) | BridgeModel::PyO3(bindings) => {
bindings.supports_free_threaded()
}
BridgeModel::Bin(None) => true,
BridgeModel::Cffi | BridgeModel::UniFfi => false,
}
}
}
impl fmt::Display for BridgeModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BridgeModel::Bin(Some(bindings)) => write!(f, "{} bin", bindings.crate_name),
BridgeModel::Bin(None) => write!(f, "bin"),
BridgeModel::PyO3(bindings) => write!(f, "{}", bindings.crate_name),
BridgeModel::Cffi => write!(f, "cffi"),
BridgeModel::UniFfi => write!(f, "uniffi"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_abi_kind_display() {
assert_eq!(StableAbiKind::Abi3.to_string(), "abi3");
assert_eq!(StableAbiKind::Abi3t.to_string(), "abi3t");
}
#[test]
fn stable_abi_kind_wheel_tag() {
assert_eq!(StableAbiKind::Abi3.wheel_tag(), "abi3");
assert_eq!(StableAbiKind::Abi3t.wheel_tag(), "abi3.abi3t");
}
#[test]
fn stable_abi_constructors() {
let abi3 = StableAbi::from_abi3_version(3, 9);
assert_eq!(abi3.kind, StableAbiKind::Abi3);
assert_eq!(abi3.version, StableAbiVersion::Version(3, 9));
let abi3t = StableAbi::from_abi3t_version(3, 15);
assert_eq!(abi3t.kind, StableAbiKind::Abi3t);
assert_eq!(abi3t.version, StableAbiVersion::Version(3, 15));
}
#[test]
fn bindings_spellings_roundtrip() {
let spellings = [
(Bindings::PyO3, "pyo3"),
(Bindings::PyO3Ffi, "pyo3-ffi"),
(Bindings::Cffi, "cffi"),
(Bindings::UniFfi, "uniffi"),
(Bindings::Bin, "bin"),
];
for (binding, spelling) in spellings {
assert_eq!(binding.as_str(), spelling);
assert_eq!(spelling.parse::<Bindings>().unwrap(), binding);
}
assert_eq!(Bindings::VARIANTS, spellings.map(|(_, spelling)| spelling));
}
#[test]
fn bindings_fromstr_rejects_unknown() {
let err = "foo".parse::<Bindings>().unwrap_err().to_string();
assert_eq!(
err,
"unknown bindings type `foo`, expected one of pyo3, pyo3-ffi, cffi, uniffi, bin"
);
}
}