Skip to content

Commit

Permalink
Exclude packages not in the dependency tree when finding bindings (#2426
Browse files Browse the repository at this point in the history
)

This fixes #2423. I've added a second (optional) PR that makes
`has_abi3` reuse the same datastructure for consistent lookups. :)

Thank you for your fantastic work on maturin! 😃 ⭐ 🍰
  • Loading branch information
mhils authored Jan 7, 2025
1 parent b1ccad4 commit 2c6b8a7
Showing 1 changed file with 61 additions and 44 deletions.
105 changes: 61 additions & 44 deletions src/build_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::pyproject_toml::ToolMaturin;
use crate::python_interpreter::{InterpreterConfig, InterpreterKind};
use crate::{Bindings, BridgeModel, BuildContext, PythonInterpreter, Target};
use anyhow::{bail, format_err, Context, Result};
use cargo_metadata::{CrateType, TargetKind};
use cargo_metadata::{CrateType, PackageId, TargetKind};
use cargo_metadata::{Metadata, Node};
use cargo_options::heading;
use pep440_rs::VersionSpecifiers;
Expand Down Expand Up @@ -934,46 +934,34 @@ fn filter_cargo_targets(
}

/// pyo3 supports building abi3 wheels if the unstable-api feature is not selected
fn has_abi3(cargo_metadata: &Metadata) -> Result<Option<(u8, u8)>> {
let resolve = cargo_metadata
.resolve
.as_ref()
.context("Expected cargo to return metadata with resolve")?;
fn has_abi3(deps: &HashMap<&str, &Node>) -> Result<Option<(u8, u8)>> {
for &lib in PYO3_BINDING_CRATES.iter() {
let pyo3_packages = resolve
.nodes
.iter()
.filter(|package| cargo_metadata[&package.id].name.as_str() == lib)
.collect::<Vec<_>>();
match pyo3_packages.as_slice() {
[pyo3_crate] => {
// Find the minimal abi3 python version. If there is none, abi3 hasn't been selected
// This parser abi3-py{major}{minor} and returns the minimal (major, minor) tuple
let abi3_selected = pyo3_crate.features.iter().any(|x| x == "abi3");
if let Some(pyo3_crate) = deps.get(lib) {
// Find the minimal abi3 python version. If there is none, abi3 hasn't been selected
// This parser abi3-py{major}{minor} and returns the minimal (major, minor) tuple
let abi3_selected = pyo3_crate.features.iter().any(|x| x == "abi3");

let min_abi3_version = pyo3_crate
.features
.iter()
.filter(|x| x.starts_with("abi3-py") && x.len() >= "abi3-pyxx".len())
.map(|x| {
Ok((
(x.as_bytes()[7] as char).to_string().parse::<u8>()?,
x[8..].parse::<u8>()?,
))
})
.collect::<Result<Vec<(u8, u8)>>>()
.context(format!("Bogus {lib} cargo features"))?
.into_iter()
.min();
if abi3_selected && min_abi3_version.is_none() {
bail!(
let min_abi3_version = pyo3_crate
.features
.iter()
.filter(|x| x.starts_with("abi3-py") && x.len() >= "abi3-pyxx".len())
.map(|x| {
Ok((
(x.as_bytes()[7] as char).to_string().parse::<u8>()?,
x[8..].parse::<u8>()?,
))
})
.collect::<Result<Vec<(u8, u8)>>>()
.context(format!("Bogus {lib} cargo features"))?
.into_iter()
.min();
if abi3_selected && min_abi3_version.is_none() {
bail!(
"You have selected the `abi3` feature but not a minimum version (e.g. the `abi3-py36` feature). \
maturin needs a minimum version feature to build abi3 wheels."
)
}
return Ok(min_abi3_version);
}
_ => continue,
return Ok(min_abi3_version);
}
}
Ok(None)
Expand Down Expand Up @@ -1034,18 +1022,47 @@ fn find_bindings(
}
}

/// Tries to determine the [BridgeModel] for the target crate
pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<BridgeModel> {
/// Return a map with all (transitive) dependencies of the *current* crate.
/// This is different from `metadata.resolve`, which also includes packages
/// that are used in the same workspace, but on which the current crate does not depend.
fn current_crate_dependencies(cargo_metadata: &Metadata) -> Result<HashMap<&str, &Node>> {
let resolve = cargo_metadata
.resolve
.as_ref()
.ok_or_else(|| format_err!("Expected to get a dependency graph from cargo"))?;
.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();

// Walk the dependency tree to get all (in)direct children.
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 deps: HashMap<&str, &Node> = resolve
.nodes
.iter()
.map(|node| (cargo_metadata[&node.id].name.as_ref(), node))
.collect();
Ok(nodes
.into_iter()
.filter_map(|(id, node)| {
dep_ids
.contains(&id)
.then_some((cargo_metadata[id].name.as_ref(), node))
})
.collect())
}

/// Tries to determine the [BridgeModel] for the target crate
pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<BridgeModel> {
let deps = current_crate_dependencies(cargo_metadata)?;
let packages: HashMap<&str, &cargo_metadata::Package> = cargo_metadata
.packages
.iter()
Expand Down Expand Up @@ -1141,7 +1158,7 @@ pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<Br
);
}

return if let Some((major, minor)) = has_abi3(cargo_metadata)? {
return if let Some((major, minor)) = has_abi3(&deps)? {
eprintln!("🔗 Found {lib} bindings with abi3 support for Python ≥ {major}.{minor}");
let version = packages[lib].version.clone();
let bindings = Bindings {
Expand Down

0 comments on commit 2c6b8a7

Please sign in to comment.