use super::{
ABI3T_MINIMUM_PYTHON_MINOR, Bindings, BridgeModel, PyO3, PyO3Crate, PyO3MetadataRaw, StableAbi,
StableAbiKind, StableAbiVersion,
};
use crate::pyproject_toml::{FeatureConditionEnv, FeatureSpec};
use crate::{CargoOptions, PyProjectToml};
use anyhow::{Context, Result, bail};
use cargo_metadata::{CrateType, Metadata, Node, PackageId, TargetKind};
use std::collections::{HashMap, HashSet};
use std::process::Command;
const PYO3_BINDING_CRATES: [PyO3Crate; 2] = [PyO3Crate::PyO3Ffi, PyO3Crate::PyO3];
pub fn find_bridge(
cargo_metadata: &Metadata,
bridge: Option<Bindings>,
cargo_options: &CargoOptions,
) -> Result<BridgeModel> {
let deps = CrateDependencies::resolve(cargo_metadata, cargo_options)?;
find_bridge_with_deps(cargo_metadata, bridge, &deps)
}
pub fn find_bridge_with_deps(
cargo_metadata: &Metadata,
bridge: Option<Bindings>,
deps: &CrateDependencies,
) -> Result<BridgeModel> {
let no_extra_features = HashMap::new();
let root_package = cargo_metadata
.root_package()
.context("Expected cargo to return metadata with root_package")?;
let targets: Vec<_> = root_package
.targets
.iter()
.filter(|target| {
target.kind.iter().any(|kind| {
!matches!(
kind,
TargetKind::Bench
| TargetKind::CustomBuild
| TargetKind::Example
| TargetKind::ProcMacro
| TargetKind::Test
)
})
})
.flat_map(|target| target.crate_types.iter().cloned())
.collect();
let bridge = if let Some(bindings) = bridge {
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)? {
Some(bindings) => {
if !targets.contains(&CrateType::CDyLib) && targets.contains(&CrateType::Bin) {
BridgeModel::Bin(Some(bindings))
} else {
BridgeModel::PyO3(bindings)
}
}
_ => {
if deps.contains("uniffi") {
BridgeModel::UniFfi
} else if targets.contains(&CrateType::CDyLib) {
BridgeModel::Cffi
} else if targets.contains(&CrateType::Bin) {
BridgeModel::Bin(find_pyo3_bindings(cargo_metadata, deps)?)
} else {
bail!(
"Couldn't detect the binding type; Please specify them with --bindings/-b"
)
}
}
}
};
if !bridge.is_pyo3() {
return Ok(bridge);
}
for &lib in PYO3_BINDING_CRATES.iter() {
if !bridge.is_bin() && bridge.is_pyo3_crate(lib) {
let lib_name = lib.as_str();
let pyo3 = bridge.pyo3().expect("should be pyo3 bindings");
if !deps.features(lib_name).contains(&"extension-module") {
let version = &pyo3.version;
if (version.major, version.minor) < (0, 26) {
eprintln!(
"⚠️ Warning: You're building a library without activating {lib}'s \
`extension-module` feature. \
See https://pyo3.rs/v{version}/building-and-distribution.html#the-extension-module-feature"
);
}
}
return if let Some(stable_abi) = has_stable_abi(deps, &no_extra_features, &[])? {
let bindings = PyO3 {
crate_name: lib,
version: pyo3.version.clone(),
stable_abi: Some(stable_abi),
metadata: pyo3.metadata.clone(),
};
Ok(BridgeModel::PyO3(bindings))
} else {
Ok(bridge)
};
}
}
Ok(bridge)
}
pub fn upgrade_bridge_stable_abi(
bridge: BridgeModel,
deps: &CrateDependencies,
pyproject: Option<&PyProjectToml>,
interpreters: &[crate::PythonInterpreter],
) -> Result<BridgeModel> {
let Some(pyo3) = bridge.pyo3() else {
return Ok(bridge);
};
let extra_pyo3_features = pyo3_features_from_conditional(pyproject, interpreters);
if let Some(stable_abi) = has_stable_abi(deps, &extra_pyo3_features, interpreters)? {
let upgraded = PyO3 {
stable_abi: Some(stable_abi),
..pyo3.clone()
};
return Ok(match bridge {
BridgeModel::PyO3(_) => BridgeModel::PyO3(upgraded),
BridgeModel::Bin(Some(_)) => BridgeModel::Bin(Some(upgraded)),
_ => return Ok(bridge),
});
}
Ok(bridge)
}
pub fn has_windows_import_lib_support(
cargo_metadata: &Metadata,
deps: &CrateDependencies,
) -> Result<bool> {
if deps.contains("pyo3") {
let Some(pyo3_crate) = deps.get_unambiguous("pyo3") else {
return Ok(false);
};
return Ok(import_lib_support_for(
cargo_metadata,
deps,
"pyo3",
pyo3_crate,
));
}
if let Some(pyo3_crate) = deps.get_unambiguous("pyo3-ffi") {
return Ok(import_lib_support_for(
cargo_metadata,
deps,
"pyo3-ffi",
pyo3_crate,
));
}
Ok(false)
}
fn import_lib_support_for(
cargo_metadata: &Metadata,
deps: &CrateDependencies,
lib: &str,
pyo3_crate: &Node,
) -> bool {
let pyo3_version = &cargo_metadata[&pyo3_crate.id].version;
if pyo3_version >= &semver::Version::new(0, 29, 0) {
return true;
}
deps.features(lib)
.iter()
.any(|&x| x == "generate-import-lib" || x == "generate-abi3-import-lib")
}
fn has_stable_abi(
deps: &CrateDependencies,
extra_features: &HashMap<&str, Vec<String>>,
interpreters: &[crate::PythonInterpreter],
) -> Result<Option<StableAbi>> {
let abi3t = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3t)?;
let abi3 = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3)?;
let selected = [abi3t, abi3].into_iter().flatten().find(|stable_abi| {
interpreters.iter().any(|interpreter| {
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)
})
})
});
Ok(selected.or(abi3).or(abi3t))
}
fn has_stable_abi_from_kind(
deps: &CrateDependencies,
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 deps.contains(lib) {
let extra = extra_features.get(lib);
let all_features: Vec<&str> = deps
.features(lib)
.into_iter()
.chain(extra.into_iter().flatten().map(String::as_str))
.collect();
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_stable_abi_version = all_features
.iter()
.filter(|&&x| x.starts_with(search_str.as_str()) && x.len() >= filter_len)
.map(|x| {
Ok((
(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_stable_abi_version {
Some((major, minor)) => {
let (major, minor) = if abi_kind == StableAbiKind::Abi3t {
(major, minor).max((3, ABI3T_MINIMUM_PYTHON_MINOR))
} else {
(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 => {}
}
}
}
Ok(None)
}
fn find_pyo3_bindings(
cargo_metadata: &Metadata,
deps: &CrateDependencies,
) -> anyhow::Result<Option<PyO3>> {
if deps.contains("pyo3") {
let node = deps
.get_unambiguous("pyo3")
.with_context(|| ambiguous_bindings_crate_msg("pyo3"))?;
let package = &cargo_metadata[&node.id];
let pyo3_metadata = if deps.contains("pyo3-ffi") {
let pyo3_ffi = deps
.get_unambiguous("pyo3-ffi")
.with_context(|| ambiguous_bindings_crate_msg("pyo3-ffi"))?;
cargo_metadata[&pyo3_ffi.id].metadata.clone()
} else {
serde_json::Value::Null
};
let metadata = match serde_json::from_value::<Option<PyO3MetadataRaw>>(pyo3_metadata) {
Ok(Some(metadata)) => Some(metadata.try_into()?),
Ok(None) | Err(_) => None,
};
Ok(Some(PyO3 {
crate_name: PyO3Crate::PyO3,
version: package.version.clone(),
stable_abi: None,
metadata,
}))
} else if deps.contains("pyo3-ffi") {
let node = deps
.get_unambiguous("pyo3-ffi")
.with_context(|| ambiguous_bindings_crate_msg("pyo3-ffi"))?;
let package = &cargo_metadata[&node.id];
let metadata =
match serde_json::from_value::<Option<PyO3MetadataRaw>>(package.metadata.clone()) {
Ok(Some(metadata)) => Some(metadata.try_into()?),
Ok(None) | Err(_) => None,
};
Ok(Some(PyO3 {
crate_name: PyO3Crate::PyO3Ffi,
version: package.version.clone(),
stable_abi: None,
metadata,
}))
} else {
Ok(None)
}
}
fn ambiguous_bindings_crate_msg(name: &str) -> String {
format!(
"multiple versions of `{name}` remain in the dependency graph after feature resolution; \
maturin cannot determine a single bindings version. Ensure the package depends on only \
one version of `{name}`"
)
}
fn current_crate_dependencies(cargo_metadata: &Metadata) -> Result<HashMap<&str, Vec<&Node>>> {
let resolve = cargo_metadata
.resolve
.as_ref()
.context("Expected to get a dependency graph from cargo")?;
let root = resolve
.root
.as_ref()
.context("expected to get a root package")?;
let nodes: HashMap<&PackageId, &Node> =
resolve.nodes.iter().map(|node| (&node.id, node)).collect();
let mut dep_ids = HashSet::with_capacity(nodes.len());
let mut todo = Vec::from([root]);
while let Some(id) = todo.pop() {
for dep in nodes[id].deps.iter() {
if dep_ids.contains(&dep.pkg) {
continue;
}
dep_ids.insert(&dep.pkg);
todo.push(&dep.pkg);
}
}
let mut deps: HashMap<&str, Vec<&Node>> = HashMap::with_capacity(dep_ids.len());
for (&id, node) in &nodes {
if !dep_ids.contains(&id) {
continue;
}
let name = cargo_metadata[id].name.as_ref();
deps.entry(name).or_default().push(node);
}
Ok(deps)
}
const BINDINGS_CRATES: [&str; 3] = ["pyo3", "pyo3-ffi", "uniffi"];
pub struct CrateDependencies<'a> {
nodes: HashMap<&'a str, Vec<&'a Node>>,
scoped_features: HashMap<(String, semver::Version), Vec<String>>,
cargo_metadata: &'a Metadata,
}
impl<'a> CrateDependencies<'a> {
pub fn resolve(cargo_metadata: &'a Metadata, cargo_options: &CargoOptions) -> Result<Self> {
let mut nodes = current_crate_dependencies(cargo_metadata)?;
let mut scoped_features = HashMap::new();
if cargo_metadata.workspace_members.len() > 1
&& BINDINGS_CRATES.iter().any(|name| nodes.contains_key(name))
{
match scoped_dependency_features(cargo_metadata, cargo_options) {
Ok(scoped) => {
for name in BINDINGS_CRATES {
let mut scoped_versions =
scoped.keys().filter_map(|(scoped_name, version)| {
(scoped_name == name).then_some(version)
});
match (scoped_versions.next(), scoped_versions.next()) {
(None, _) => {
nodes.remove(name);
}
(Some(version), None) => {
if let Some(node) = unique_node_by_version(
cargo_metadata,
nodes.get(name).map(Vec::as_slice).unwrap_or(&[]),
version,
) {
nodes.insert(name, vec![node]);
}
}
(Some(_), Some(_)) => {
}
}
}
scoped_features = scoped;
}
Err(err) => {
eprintln!(
"⚠️ Warning: Failed to verify the dependency graph with `cargo tree`, \
bindings detection may mistake feature-gated dependencies of other \
workspace members as enabled: {err:#}"
);
}
}
}
Ok(Self {
nodes,
scoped_features,
cargo_metadata,
})
}
fn contains(&self, name: &str) -> bool {
self.nodes.get(name).is_some_and(|nodes| !nodes.is_empty())
}
fn get_unambiguous(&self, name: &str) -> Option<&'a Node> {
match self.nodes.get(name).map(Vec::as_slice)? {
[node] => Some(*node),
_ => None,
}
}
fn features(&self, name: &str) -> Vec<&str> {
match self.nodes.get(name).map(Vec::as_slice) {
Some([node]) => {
if let Some(features) = self.scoped_for(name, node) {
features.iter().map(String::as_str).collect()
} else {
node.features.iter().map(AsRef::as_ref).collect()
}
}
Some(nodes) if nodes.len() > 1 => self.common_scoped_features(name),
_ => Vec::new(),
}
}
fn common_scoped_features(&self, name: &str) -> Vec<&str> {
let mut versions = self
.scoped_features
.iter()
.filter(|((scoped_name, _), _)| scoped_name == name)
.map(|(_, features)| features);
let Some(first) = versions.next() else {
return Vec::new();
};
let mut common: Vec<&str> = first.iter().map(String::as_str).collect();
for features in versions {
common.retain(|feature| features.iter().any(|existing| existing == feature));
}
common
}
fn scoped_for(&self, name: &str, node: &Node) -> Option<&Vec<String>> {
let version = &self.cargo_metadata[&node.id].version;
self.scoped_features
.get(&(name.to_string(), version.clone()))
}
}
fn unique_node_by_version<'a>(
cargo_metadata: &Metadata,
candidates: &[&'a Node],
version: &semver::Version,
) -> Option<&'a Node> {
let mut matches = candidates
.iter()
.copied()
.filter(|node| &cargo_metadata[&node.id].version == version);
match (matches.next(), matches.next()) {
(Some(node), None) => Some(node),
_ => None,
}
}
fn scoped_dependency_features(
cargo_metadata: &Metadata,
cargo_options: &CargoOptions,
) -> Result<HashMap<(String, semver::Version), Vec<String>>> {
let root_package = cargo_metadata
.root_package()
.context("Expected cargo to return metadata with root_package")?;
let mut cmd = Command::new("cargo");
cmd.arg("tree")
.arg("--manifest-path")
.arg(root_package.manifest_path.as_std_path())
.args([
"--edges",
"normal,build,dev",
"--prefix",
"none",
"--format",
"{p}|{f}",
"--color",
"never",
]);
cmd.args(cargo_options.cargo_tree_args());
let output = cmd
.output()
.context("Failed to run `cargo tree`. Do you have cargo in your PATH?")?;
if !output.status.success() {
bail!(
"`cargo tree` exited with {}:\n{}",
output.status,
String::from_utf8_lossy(&output.stderr),
);
}
let mut scoped: HashMap<(String, semver::Version), Vec<String>> = HashMap::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
let line = line.trim_end().trim_end_matches(" (*)");
let Some((package, features)) = line.split_once('|') else {
continue;
};
let mut package_parts = package.split_whitespace();
let Some(name) = package_parts.next() else {
continue;
};
if !BINDINGS_CRATES.contains(&name) {
continue;
}
let version = package_parts
.next()
.and_then(|v| v.strip_prefix('v'))
.and_then(|v| semver::Version::parse(v).ok())
.with_context(|| format!("unexpected `cargo tree` output line: {line}"))?;
let entry = scoped.entry((name.to_string(), version)).or_default();
for feature in features.split(',').filter(|feature| !feature.is_empty()) {
if !entry.iter().any(|existing| existing == feature) {
entry.push(feature.to_string());
}
}
}
Ok(scoped)
}
fn pyo3_features_from_conditional(
pyproject: Option<&PyProjectToml>,
interpreters: &[crate::PythonInterpreter],
) -> HashMap<&'static str, Vec<String>> {
let mut extra: HashMap<&'static str, Vec<String>> = HashMap::new();
let features = match pyproject
.and_then(|p| p.maturin())
.and_then(|m| m.features.clone())
{
Some(f) => f,
None => return extra,
};
let (_plain, conditional) = FeatureSpec::split(features);
let crate_names: &[&str] = &["pyo3", "pyo3-ffi"];
let mut seen = HashSet::new();
for interp in interpreters {
let env = FeatureConditionEnv {
major: interp.major,
minor: interp.minor,
implementation_name: &interp.implementation_name,
};
for feature in FeatureSpec::resolve_conditional(&conditional, &env) {
if seen.insert(feature.clone()) {
for &crate_name in crate_names {
let prefix = format!("{crate_name}/");
if let Some(feat_name) = feature.strip_prefix(&prefix) {
extra
.entry(crate_name)
.or_default()
.push(feat_name.to_string());
}
}
}
}
}
extra
}