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
40 changes: 37 additions & 3 deletions maturin.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@
},
"bindings": {
"description": "Bindings type",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/$defs/Bindings"
},
{
"type": "null"
}
]
},
"compatibility": {
Expand Down Expand Up @@ -272,6 +276,36 @@
}
]
},
"Bindings": {
"description": "The kind of bindings to use.",
"oneOf": [
{
"description": "PyO3 bindings",
"type": "string",
"const": "pyo3"
},
{
"description": "pyo3-ffi (raw FFI) bindings",
"type": "string",
"const": "pyo3-ffi"
},
{
"description": "CFFI bindings",
"type": "string",
"const": "cffi"
},
{
"description": "UniFFI bindings",
"type": "string",
"const": "uniffi"
},
{
"description": "Rust binary",
"type": "string",
"const": "bin"
}
]
},
"CargoCrateType": {
"description": "Supported cargo crate types",
"oneOf": [
Expand Down
29 changes: 15 additions & 14 deletions src/bridge/detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! whether abi3 is enabled, and whether `generate-import-lib` is active.

use super::{
ABI3T_MINIMUM_PYTHON_MINOR, BridgeModel, PyO3, PyO3Crate, PyO3MetadataRaw, StableAbi,
ABI3T_MINIMUM_PYTHON_MINOR, Bindings, BridgeModel, PyO3, PyO3Crate, PyO3MetadataRaw, StableAbi,
StableAbiKind, StableAbiVersion,
};
use crate::pyproject_toml::{FeatureConditionEnv, FeatureSpec};
Expand All @@ -29,7 +29,7 @@ const PYO3_BINDING_CRATES: [PyO3Crate; 2] = [PyO3Crate::PyO3Ffi, PyO3Crate::PyO3
/// to evaluate them.
pub fn find_bridge(
cargo_metadata: &Metadata,
bridge: Option<&str>,
bridge: Option<Bindings>,
cargo_options: &CargoOptions,
) -> Result<BridgeModel> {
let deps = CrateDependencies::resolve(cargo_metadata, cargo_options)?;
Expand All @@ -40,7 +40,7 @@ pub fn find_bridge(
/// also need [`CrateDependencies`] elsewhere resolve it only once.
pub fn find_bridge_with_deps(
cargo_metadata: &Metadata,
bridge: Option<&str>,
bridge: Option<Bindings>,
deps: &CrateDependencies,
) -> Result<BridgeModel> {
let no_extra_features = HashMap::new();
Expand All @@ -66,17 +66,18 @@ pub fn find_bridge_with_deps(
.collect();

let bridge = if let Some(bindings) = bridge {
if bindings == "cffi" {
BridgeModel::Cffi
} else if bindings == "uniffi" {
BridgeModel::UniFfi
} else if bindings == "bin" {
let bindings = find_pyo3_bindings(cargo_metadata, deps)?;
BridgeModel::Bin(bindings)
} else {
let bindings =
find_pyo3_bindings(cargo_metadata, deps)?.context("unknown binding type")?;
BridgeModel::PyO3(bindings)
match bindings {
Bindings::Cffi => BridgeModel::Cffi,
Bindings::UniFfi => BridgeModel::UniFfi,
Bindings::Bin => {
let bindings = find_pyo3_bindings(cargo_metadata, deps)?;
BridgeModel::Bin(bindings)
}
Bindings::PyO3 | Bindings::PyO3Ffi => {
let bindings =
find_pyo3_bindings(cargo_metadata, deps)?.context("unknown binding type")?;
BridgeModel::PyO3(bindings)
}
}
} else {
match find_pyo3_bindings(cargo_metadata, deps)? {
Expand Down
204 changes: 203 additions & 1 deletion src/bridge/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub use detection::{
use std::{fmt, str::FromStr};

use anyhow::Context;
use serde::Deserialize;
use serde::{Deserialize, Serialize};

use crate::python_interpreter::{
MAXIMUM_PYPY_MINOR, MAXIMUM_PYTHON_MINOR, MINIMUM_PYPY_MINOR, MINIMUM_PYTHON_MINOR,
Expand Down Expand Up @@ -61,6 +61,178 @@ impl FromStr for PyO3Crate {
}
}

/// The `bindings` input spelling accepted on the CLI (`--bindings`) and in
/// `[tool.maturin] bindings`.
///
/// This is the raw, user-facing choice of binding model, kept deliberately
/// distinct from the resolved [`BridgeModel`]: auto-detection and resolved PyO3
/// metadata are semantic build states, whereas this enum only names the
/// accepted input spellings. [`find_bridge`] converts it into a [`BridgeModel`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Bindings {
/// pyo3
PyO3,
/// pyo3-ffi
PyO3Ffi,
/// cffi
Cffi,
/// uniffi
UniFfi,
/// bin
Bin,
}

impl Bindings {
/// All accepted bindings, in declaration order.
pub const ALL: [Bindings; 5] = [
Bindings::PyO3,
Bindings::PyO3Ffi,
Bindings::Cffi,
Bindings::UniFfi,
Bindings::Bin,
];

/// The accepted spellings as strings, derived from [`Bindings::as_str`].
///
/// This is the single list handed to serde, clap and schemars when they
/// need to enumerate or reject variants.
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(),
];

/// Returns the canonical spelling of this bindings type.
///
/// This is the one place each spelling is written; `FromStr`, `Display`,
/// serde, clap and schemars all derive from it.
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",
}
}

/// A one-line human description of this bindings type, used as the
/// per-variant documentation in the generated JSON schema.
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 {
// Document the string form (e.g. "pyo3", "cffi") so the schema matches
// serde Deserialize/Serialize rather than an externally-tagged object.
// Emit one `const` per variant with a description, mirroring the house
// style of the other documented string enums (e.g. `CargoCrateType`).
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")]
Expand Down Expand Up @@ -439,4 +611,34 @@ mod tests {
assert_eq!(abi3t.kind, StableAbiKind::Abi3t);
assert_eq!(abi3t.version, StableAbiVersion::Version(3, 15));
}

#[test]
fn bindings_spellings_roundtrip() {
// The exact accepted spellings, in the historical order. These are a
// public contract (CLI values, `[tool.maturin] bindings`, JSON schema)
// and are not something the type can guarantee: only pinning the
// literals catches an accidental edit to `as_str`. Hardcoded here (not
// derived from `as_str`/`VARIANTS`) so the round-trip is meaningful.
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"
);
}
}
2 changes: 1 addition & 1 deletion src/build_context/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl BuildContextBuilder {
)?;
let pyproject = pyproject_toml.as_ref();

let bindings = build_options.python.bindings.as_deref().or_else(|| {
let bindings = build_options.python.bindings.or_else(|| {
pyproject.and_then(|x| {
if x.bindings().is_some() {
pyproject_toml_maturin_options.push("bindings");
Expand Down
Loading
Loading