Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude packages not in the dependency tree when finding bindings #2426

Merged
merged 2 commits into from
Jan 7, 2025
Merged
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
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
Loading