maturin 1.13.0

Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages
Documentation
mod detection;

pub use detection::{find_bridge, is_generating_import_lib};

use std::{fmt, str::FromStr};

use anyhow::Context;
use serde::Deserialize;

use crate::python_interpreter::{
    MAXIMUM_PYPY_MINOR, MAXIMUM_PYTHON_MINOR, MINIMUM_PYPY_MINOR, MINIMUM_PYTHON_MINOR,
};

/// pyo3 binding crate
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PyO3Crate {
    /// pyo3
    PyO3,
    /// pyo3-ffi
    PyO3Ffi,
}

impl PyO3Crate {
    /// Returns the name of the crate as a string
    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, 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)?,
        })
    }
}

/// 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,
    /// 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 {
    /// The name of the bindings crate, `pyo3` or `uniffi`
    pub crate_name: PyO3Crate,
    /// pyo3 bindings crate version
    pub version: semver::Version,
    /// abi3 support
    pub stable_abi: Option<StableAbi>,
    /// pyo3 metadata
    pub metadata: Option<PyO3Metadata>,
}

impl PyO3 {
    /// Returns the minimum python minor version supported
    fn minimal_python_minor_version(&self) -> usize {
        let major_version = self.version.major;
        let minor_version = self.version.minor;
        // N.B. must check large minor versions first
        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
        }
    }

    /// Returns the maximum python minor version supported
    fn maximum_python_minor_version(&self) -> usize {
        // N.B. must check large minor versions first
        if let Some(metadata) = self.metadata.as_ref() {
            metadata.cpython.max_minor
        } else {
            MAXIMUM_PYTHON_MINOR
        }
    }

    /// Returns the minimum PyPy minor version supported
    fn minimal_pypy_minor_version(&self) -> usize {
        let major_version = self.version.major;
        let minor_version = self.version.minor;
        // N.B. must check large minor versions first
        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
        }
    }

    /// Returns the maximum PyPy minor version supported
    fn maximum_pypy_minor_version(&self) -> usize {
        // N.B. must check large minor versions first
        if let Some(metadata) = self.metadata.as_ref() {
            metadata.pypy.max_minor
        } else {
            MAXIMUM_PYPY_MINOR
        }
    }

    /// free-threaded Python support
    fn supports_free_threaded(&self) -> bool {
        let major_version = self.version.major;
        let minor_version = self.version.minor;
        (major_version, minor_version) >= (0, 23)
    }
}

/// The way the rust code is used in the wheel
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum BridgeModel {
    /// A rust binary to be shipped a python package
    Bin(Option<PyO3>),
    /// A native module with pyo3 bindings.
    PyO3(PyO3),
    /// A native module with c bindings, i.e. `#[no_mangle] extern "C" <some item>`
    Cffi,
    /// A native module generated from uniffi
    UniFfi,
}

impl BridgeModel {
    /// Returns the pyo3 bindings
    pub fn pyo3(&self) -> Option<&PyO3> {
        match self {
            BridgeModel::Bin(Some(bindings)) => Some(bindings),
            BridgeModel::PyO3(bindings) => Some(bindings),
            _ => None,
        }
    }

    /// Test whether this is using pyo3/pyo3-ffi
    pub fn is_pyo3(&self) -> bool {
        matches!(self, BridgeModel::PyO3(_) | BridgeModel::Bin(Some(_)))
    }

    /// Test whether this is using a specific pyo3 crate
    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,
        }
    }

    /// Test whether this is bin bindings
    pub fn is_bin(&self) -> bool {
        matches!(self, BridgeModel::Bin(_))
    }

    /// Returns the minimum python minor version supported
    pub fn minimal_python_minor_version(&self) -> usize {
        match self.pyo3() {
            Some(bindings) => bindings.minimal_python_minor_version(),
            None => MINIMUM_PYTHON_MINOR,
        }
    }

    /// Returns the maximum python minor version supported
    pub fn maximum_python_minor_version(&self) -> usize {
        match self.pyo3() {
            Some(bindings) => bindings.maximum_python_minor_version(),
            None => MAXIMUM_PYTHON_MINOR,
        }
    }

    /// Returns the minimum PyPy minor version supported
    pub fn minimal_pypy_minor_version(&self) -> usize {
        match self.pyo3() {
            Some(bindings) => bindings.minimal_pypy_minor_version(),
            None => MINIMUM_PYPY_MINOR,
        }
    }

    /// Returns the maximum PyPy minor version supported
    pub fn maximum_pypy_minor_version(&self) -> usize {
        use crate::python_interpreter::MAXIMUM_PYPY_MINOR;

        match self.pyo3() {
            Some(bindings) => bindings.maximum_pypy_minor_version(),
            None => MAXIMUM_PYPY_MINOR,
        }
    }

    /// Is using abi3
    pub fn is_abi3(&self) -> bool {
        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
    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"),
        }
    }
}