use crate::auditwheel::PlatformTag;
use crate::build_context::BridgeModel;
use crate::compile::{CompileTarget, LIB_CRATE_TYPES};
use crate::cross_compile::{find_sysconfigdata, parse_sysconfigdata};
use crate::project_layout::ProjectResolver;
use crate::pyproject_toml::ToolMaturin;
use crate::python_interpreter::{InterpreterConfig, InterpreterKind, MINIMUM_PYTHON_MINOR};
use crate::{BuildContext, PythonInterpreter, Target};
use anyhow::{bail, format_err, Context, Result};
use cargo_metadata::{Metadata, Node};
use cargo_options::heading;
use pep440_rs::VersionSpecifiers;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::env;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
use tracing::debug;
const PYO3_BINDING_CRATES: [&str; 2] = ["pyo3-ffi", "pyo3"];
fn pyo3_minimum_python_minor_version(major_version: u64, minor_version: u64) -> Option<usize> {
if (major_version, minor_version) >= (0, 16) {
Some(7)
} else {
None
}
}
fn pyo3_ffi_minimum_python_minor_version(major_version: u64, minor_version: u64) -> Option<usize> {
if (major_version, minor_version) >= (0, 16) {
pyo3_minimum_python_minor_version(major_version, minor_version)
} else {
None
}
}
#[derive(Debug, Default, Serialize, Deserialize, clap::Parser, Clone, Eq, PartialEq)]
#[serde(default, rename_all = "kebab-case")]
pub struct CargoOptions {
#[arg(short = 'q', long)]
pub quiet: bool,
#[arg(short = 'j', long, value_name = "N", help_heading = heading::COMPILATION_OPTIONS)]
pub jobs: Option<usize>,
#[arg(long, value_name = "PROFILE-NAME", help_heading = heading::COMPILATION_OPTIONS)]
pub profile: Option<String>,
#[arg(
short = 'F',
long,
action = clap::ArgAction::Append,
help_heading = heading::FEATURE_SELECTION,
)]
pub features: Vec<String>,
#[arg(long, help_heading = heading::FEATURE_SELECTION)]
pub all_features: bool,
#[arg(long, help_heading = heading::FEATURE_SELECTION)]
pub no_default_features: bool,
#[arg(
long,
value_name = "TRIPLE",
env = "CARGO_BUILD_TARGET",
help_heading = heading::COMPILATION_OPTIONS,
)]
pub target: Option<String>,
#[arg(long, value_name = "DIRECTORY", help_heading = heading::COMPILATION_OPTIONS)]
pub target_dir: Option<PathBuf>,
#[arg(short = 'm', long, value_name = "PATH", help_heading = heading::MANIFEST_OPTIONS)]
pub manifest_path: Option<PathBuf>,
#[arg(long)]
pub ignore_rust_version: bool,
#[arg(short = 'v', long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long, value_name = "WHEN")]
pub color: Option<String>,
#[arg(long, help_heading = heading::MANIFEST_OPTIONS)]
pub frozen: bool,
#[arg(long, help_heading = heading::MANIFEST_OPTIONS)]
pub locked: bool,
#[arg(long, help_heading = heading::MANIFEST_OPTIONS)]
pub offline: bool,
#[arg(long, value_name = "KEY=VALUE", action = clap::ArgAction::Append)]
pub config: Vec<String>,
#[arg(short = 'Z', value_name = "FLAG", action = clap::ArgAction::Append)]
pub unstable_flags: Vec<String>,
#[arg(
long,
value_name = "FMTS",
value_delimiter = ',',
require_equals = true,
help_heading = heading::COMPILATION_OPTIONS,
)]
pub timings: Option<Vec<String>>,
#[arg(long)]
pub future_incompat_report: bool,
#[arg(num_args = 0.., trailing_var_arg = true)]
pub args: Vec<String>,
}
#[derive(Debug, Default, Serialize, Deserialize, clap::Parser, Clone, Eq, PartialEq)]
#[serde(default)]
pub struct BuildOptions {
#[arg(
id = "compatibility",
long = "compatibility",
alias = "manylinux",
num_args = 0..,
action = clap::ArgAction::Append
)]
pub platform_tag: Vec<PlatformTag>,
#[arg(short, long, num_args = 0.., action = clap::ArgAction::Append)]
pub interpreter: Vec<PathBuf>,
#[arg(short = 'f', long, conflicts_with = "interpreter")]
pub find_interpreter: bool,
#[arg(short, long, value_parser = ["pyo3", "pyo3-ffi", "rust-cpython", "cffi", "uniffi", "bin"])]
pub bindings: Option<String>,
#[arg(short, long)]
pub out: Option<PathBuf>,
#[arg(long = "skip-auditwheel")]
pub skip_auditwheel: bool,
#[cfg(feature = "zig")]
#[arg(long)]
pub zig: bool,
#[command(flatten)]
pub cargo: CargoOptions,
}
impl Deref for BuildOptions {
type Target = CargoOptions;
fn deref(&self) -> &Self::Target {
&self.cargo
}
}
impl DerefMut for BuildOptions {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cargo
}
}
impl BuildOptions {
fn find_interpreters(
&self,
bridge: &BridgeModel,
interpreter: &[PathBuf],
target: &Target,
requires_python: Option<&VersionSpecifiers>,
generate_import_lib: bool,
) -> Result<Vec<PythonInterpreter>> {
match bridge {
BridgeModel::Bindings(binding_name, _) | BridgeModel::Bin(Some((binding_name, _))) => {
let mut interpreters = Vec::new();
if let Some(config_file) = env::var_os("PYO3_CONFIG_FILE") {
if !binding_name.starts_with("pyo3") {
bail!("Only pyo3 bindings can be configured with PYO3_CONFIG_FILE");
}
let interpreter_config =
InterpreterConfig::from_pyo3_config(config_file.as_ref(), target)
.context("Invalid PYO3_CONFIG_FILE")?;
interpreters.push(PythonInterpreter::from_config(interpreter_config));
} else if binding_name.starts_with("pyo3") && target.cross_compiling() {
if let Some(cross_lib_dir) = env::var_os("PYO3_CROSS_LIB_DIR") {
let host_interpreters =
find_interpreter_in_host(bridge, interpreter, target, requires_python)?;
let host_python = &host_interpreters[0];
eprintln!("🐍 Using host {host_python} for cross-compiling preparation");
env::set_var("PYO3_PYTHON", &host_python.executable);
env::set_var("PYTHON_SYS_EXECUTABLE", &host_python.executable);
let sysconfig_path = find_sysconfigdata(cross_lib_dir.as_ref(), target)?;
env::set_var(
"MATURIN_PYTHON_SYSCONFIGDATA_DIR",
sysconfig_path.parent().unwrap(),
);
let sysconfig_data = parse_sysconfigdata(host_python, sysconfig_path)?;
let major = sysconfig_data
.get("version_major")
.context("version_major is not defined")?
.parse::<usize>()
.context("Could not parse value of version_major")?;
let minor = sysconfig_data
.get("version_minor")
.context("version_minor is not defined")?
.parse::<usize>()
.context("Could not parse value of version_minor")?;
let abiflags = sysconfig_data
.get("ABIFLAGS")
.map(ToString::to_string)
.unwrap_or_default();
let ext_suffix = sysconfig_data
.get("EXT_SUFFIX")
.context("syconfig didn't define an `EXT_SUFFIX` ಠ_ಠ")?;
let soabi = sysconfig_data.get("SOABI");
let interpreter_kind = soabi
.and_then(|tag| {
if tag.starts_with("pypy") {
Some(InterpreterKind::PyPy)
} else if tag.starts_with("cpython") {
Some(InterpreterKind::CPython)
} else if tag.starts_with("graalpy") {
Some(InterpreterKind::GraalPy)
} else {
None
}
})
.context("unsupported Python interpreter")?;
interpreters.push(PythonInterpreter {
config: InterpreterConfig {
major,
minor,
interpreter_kind,
abiflags,
ext_suffix: ext_suffix.to_string(),
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implementation_name: interpreter_kind.to_string().to_ascii_lowercase(),
soabi: soabi.cloned(),
});
} else {
if interpreter.is_empty() && !self.find_interpreter {
bail!("Couldn't find any python interpreters. Please specify at least one with -i");
}
for interp in interpreter {
if interp.components().count() > 1
&& PythonInterpreter::check_executable(interp, target, bridge)?
.is_none()
{
bail!("{} is not a valid python interpreter", interp.display());
}
}
interpreters =
find_interpreter_in_sysconfig(interpreter, target, requires_python)?;
if interpreters.is_empty() {
bail!(
"Couldn't find any python interpreters from '{}'. Please check that both major and minor python version have been specified in -i/--interpreter.",
interpreter
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ")
);
}
}
} else if binding_name.starts_with("pyo3") {
interpreters = find_interpreter(bridge, interpreter, target, requires_python)?;
} else {
interpreters =
find_interpreter_in_host(bridge, interpreter, target, requires_python)?;
}
let interpreters_str = interpreters
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(", ");
eprintln!("🐍 Found {interpreters_str}");
Ok(interpreters)
}
BridgeModel::Cffi => {
let interpreter =
find_single_python_interpreter(bridge, interpreter, target, "cffi")?;
eprintln!("🐍 Using {interpreter} to generate the cffi bindings");
Ok(vec![interpreter])
}
BridgeModel::Bin(None) | BridgeModel::UniFfi => Ok(vec![]),
BridgeModel::BindingsAbi3(major, minor) => {
if target.is_windows() {
let interpreters =
find_interpreter_in_host(bridge, interpreter, target, requires_python)
.unwrap_or_default();
if env::var_os("PYO3_CROSS_LIB_DIR").is_some() {
eprintln!("⚠️ Cross-compiling is poorly supported");
Ok(vec![PythonInterpreter {
config: InterpreterConfig {
major: *major as usize,
minor: *minor as usize,
interpreter_kind: InterpreterKind::CPython,
abiflags: "".to_string(),
ext_suffix: ".pyd".to_string(),
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implementation_name: "cpython".to_string(),
soabi: None,
}])
} else if let Some(config_file) = env::var_os("PYO3_CONFIG_FILE") {
let interpreter_config =
InterpreterConfig::from_pyo3_config(config_file.as_ref(), target)
.context("Invalid PYO3_CONFIG_FILE")?;
Ok(vec![PythonInterpreter::from_config(interpreter_config)])
} else if let Some(interp) = interpreters.first() {
eprintln!("🐍 Using {interp} to generate to link bindings (With abi3, an interpreter is only required on windows)");
Ok(interpreters)
} else if generate_import_lib {
eprintln!("🐍 Not using a specific python interpreter (Automatically generating windows import library)");
Ok(vec![PythonInterpreter {
config: InterpreterConfig {
major: *major as usize,
minor: *minor as usize,
interpreter_kind: InterpreterKind::CPython,
abiflags: "".to_string(),
ext_suffix: ".pyd".to_string(),
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implementation_name: "cpython".to_string(),
soabi: None,
}])
} else {
bail!("Failed to find a python interpreter");
}
} else {
let found_interpreters =
find_interpreter_in_host(bridge, interpreter, target, requires_python)
.or_else(|err| {
let interps = find_interpreter_in_sysconfig(
interpreter,
target,
requires_python,
)
.unwrap_or_default();
if interps.is_empty() && !self.interpreter.is_empty() {
Err(err)
} else {
Ok(interps)
}
})?;
eprintln!("🐍 Not using a specific python interpreter");
if self.interpreter.is_empty() {
Ok(vec![PythonInterpreter {
config: InterpreterConfig {
major: *major as usize,
minor: *minor as usize,
interpreter_kind: InterpreterKind::CPython,
abiflags: "".to_string(),
ext_suffix: "".to_string(),
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implementation_name: "cpython".to_string(),
soabi: None,
}])
} else if target.cross_compiling() {
let mut interps = Vec::with_capacity(found_interpreters.len());
let mut pypys = Vec::new();
for interp in found_interpreters {
if interp.interpreter_kind.is_pypy() {
pypys.push(PathBuf::from(format!(
"pypy{}.{}",
interp.major, interp.minor
)));
} else {
interps.push(interp);
}
}
if !pypys.is_empty() {
interps.extend(find_interpreter_in_sysconfig(
&pypys,
target,
requires_python,
)?)
}
if interps.is_empty() {
bail!("Failed to find any python interpreter");
}
Ok(interps)
} else {
if found_interpreters.is_empty() {
bail!("Failed to find any python interpreter");
}
Ok(found_interpreters)
}
}
}
}
}
pub fn into_build_context(
self,
release: bool,
strip: bool,
editable: bool,
) -> Result<BuildContext> {
let ProjectResolver {
project_layout,
cargo_toml_path,
cargo_toml,
pyproject_toml_path,
pyproject_toml,
module_name,
metadata23,
mut cargo_options,
cargo_metadata,
mut pyproject_toml_maturin_options,
} = ProjectResolver::resolve(self.manifest_path.clone(), self.cargo.clone())?;
let pyproject = pyproject_toml.as_ref();
let bridge = find_bridge(
&cargo_metadata,
self.bindings.as_deref().or_else(|| {
pyproject.and_then(|x| {
if x.bindings().is_some() {
pyproject_toml_maturin_options.push("bindings");
}
x.bindings()
})
}),
)?;
if !bridge.is_bin() && project_layout.extension_name.contains('-') {
bail!(
"The module name must not contain a minus `-` \
(Make sure you have set an appropriate [lib] name or \
[tool.maturin] module-name in your pyproject.toml)"
);
}
let mut target_triple = self.target.clone();
let mut universal2 = target_triple.as_deref() == Some("universal2-apple-darwin");
if target_triple.is_none() {
if let Ok(arch_flags) = env::var("ARCHFLAGS") {
let arches: HashSet<&str> = arch_flags
.split("-arch")
.filter_map(|x| {
let x = x.trim();
if x.is_empty() {
None
} else {
Some(x)
}
})
.collect();
match (arches.contains("x86_64"), arches.contains("arm64")) {
(true, true) => universal2 = true,
(true, false) => target_triple = Some("x86_64-apple-darwin".to_string()),
(false, true) => target_triple = Some("aarch64-apple-darwin".to_string()),
(false, false) => {}
}
};
}
if universal2 {
target_triple = None;
}
let target = Target::from_target_triple(target_triple)?;
let wheel_dir = match self.out {
Some(ref dir) => dir.clone(),
None => PathBuf::from(&cargo_metadata.target_directory).join("wheels"),
};
let generate_import_lib = is_generating_import_lib(&cargo_metadata)?;
let interpreter = if self.find_interpreter {
self.find_interpreters(
&bridge,
&[],
&target,
metadata23.requires_python.as_ref(),
generate_import_lib,
)?
} else {
let interpreter = if self.interpreter.is_empty() && !target.cross_compiling() {
if cfg!(test) {
match env::var_os("MATURIN_TEST_PYTHON") {
Some(python) => vec![python.into()],
None => vec![target.get_python()],
}
} else {
vec![target.get_python()]
}
} else {
#[allow(clippy::redundant_clone)]
self.interpreter.clone()
};
self.find_interpreters(&bridge, &interpreter, &target, None, generate_import_lib)?
};
if cargo_options.args.is_empty() {
let tool_maturin = pyproject.and_then(|p| p.maturin());
if let Some(args) = tool_maturin.and_then(|x| x.rustc_args.as_ref()) {
cargo_options.args.extend(args.iter().cloned());
pyproject_toml_maturin_options.push("rustc-args");
}
}
let strip = pyproject.map(|x| x.strip()).unwrap_or_default() || strip;
let skip_auditwheel =
pyproject.map(|x| x.skip_auditwheel()).unwrap_or_default() || self.skip_auditwheel;
let platform_tags = if self.platform_tag.is_empty() {
#[cfg(feature = "zig")]
let use_zig = self.zig;
#[cfg(not(feature = "zig"))]
let use_zig = false;
let compatibility = pyproject
.and_then(|x| {
if x.compatibility().is_some() {
pyproject_toml_maturin_options.push("compatibility");
}
x.compatibility()
})
.or(if use_zig {
if target.is_musl_libc() {
Some(PlatformTag::Musllinux { x: 1, y: 2 })
} else {
Some(target.get_minimum_manylinux_tag())
}
} else {
if target.is_musl_libc() && !bridge.is_bin() {
Some(PlatformTag::Musllinux { x: 1, y: 2 })
} else {
None
}
});
if let Some(platform_tag) = compatibility {
vec![platform_tag]
} else {
Vec::new()
}
} else {
self.platform_tag
};
for platform_tag in &platform_tags {
if !platform_tag.is_supported() {
eprintln!("⚠️ Warning: {platform_tag} is unsupported by the Rust compiler.");
} else if platform_tag.is_musllinux() && !target.is_musl_libc() {
eprintln!("⚠️ Warning: {target} is not compatible with {platform_tag}.");
}
}
validate_bridge_type(&bridge, &target, &platform_tags)?;
if platform_tags.len() > 1 && platform_tags.iter().any(|tag| !tag.is_portable()) {
bail!("Cannot mix linux and manylinux/musllinux platform tags",);
}
if !pyproject_toml_maturin_options.is_empty() {
eprintln!(
"📡 Using build options {} from pyproject.toml",
pyproject_toml_maturin_options.join(", ")
);
}
let target_dir = self
.cargo
.target_dir
.clone()
.unwrap_or_else(|| cargo_metadata.target_directory.clone().into_std_path_buf());
let config_targets = pyproject.and_then(|x| x.targets());
let compile_targets =
filter_cargo_targets(&cargo_metadata, bridge, config_targets.as_deref())?;
if compile_targets.is_empty() {
bail!("No Cargo targets to build, please check your bindings configuration in pyproject.toml.");
}
let crate_name = cargo_toml.package.name;
Ok(BuildContext {
target,
compile_targets,
project_layout,
pyproject_toml_path,
pyproject_toml,
metadata23,
crate_name,
module_name,
manifest_path: cargo_toml_path,
target_dir,
out: wheel_dir,
release,
strip,
skip_auditwheel,
#[cfg(feature = "zig")]
zig: self.zig,
platform_tag: platform_tags,
interpreter,
cargo_metadata,
universal2,
editable,
cargo_options,
})
}
}
fn validate_bridge_type(
bridge: &BridgeModel,
target: &Target,
platform_tags: &[PlatformTag],
) -> Result<()> {
match bridge {
BridgeModel::Bin(None) => {
if platform_tags.iter().any(|tag| tag.is_musllinux()) && !target.is_musl_libc() {
bail!(
"Cannot mix musllinux and manylinux platform tags when compiling to {}",
target.target_triple()
);
}
#[allow(clippy::comparison_chain)]
if platform_tags.len() > 2 {
bail!(
"Expected only one or two platform tags but found {}",
platform_tags.len()
);
} else if platform_tags.len() == 2 {
let tag_types = platform_tags
.iter()
.map(|tag| tag.is_musllinux())
.collect::<HashSet<_>>();
if tag_types.len() == 1 {
bail!(
"Expected only one platform tag but found {}",
platform_tags.len()
);
}
}
}
_ => {
if platform_tags.len() > 1 {
bail!(
"Expected only one platform tag but found {}",
platform_tags.len()
);
}
}
}
Ok(())
}
fn filter_cargo_targets(
cargo_metadata: &Metadata,
bridge: BridgeModel,
config_targets: Option<&[crate::pyproject_toml::CargoTarget]>,
) -> Result<Vec<CompileTarget>> {
let root_pkg = cargo_metadata.root_package().unwrap();
let resolved_features = cargo_metadata
.resolve
.as_ref()
.and_then(|resolve| resolve.nodes.iter().find(|node| node.id == root_pkg.id))
.map(|node| node.features.clone())
.unwrap_or_default();
let mut targets: Vec<_> = root_pkg
.targets
.iter()
.filter(|target| match bridge {
BridgeModel::Bin(_) => {
let is_bin = target.is_bin();
if target.required_features.is_empty() {
is_bin
} else {
is_bin
&& target
.required_features
.iter()
.all(|f| resolved_features.contains(f))
}
}
_ => target.kind.contains(&"cdylib".to_string()),
})
.map(|target| CompileTarget {
target: target.clone(),
bridge_model: bridge.clone(),
})
.collect();
if targets.is_empty() && !bridge.is_bin() {
let lib_target = root_pkg.targets.iter().find(|target| {
target
.kind
.iter()
.any(|k| LIB_CRATE_TYPES.contains(&k.as_str()))
});
if let Some(target) = lib_target {
targets.push(CompileTarget {
target: target.clone(),
bridge_model: bridge,
});
}
}
if let Some(config_targets) = config_targets {
targets.retain(|CompileTarget { target, .. }| {
config_targets.iter().any(|config_target| {
let name_eq = config_target.name == target.name;
match &config_target.kind {
Some(kind) => name_eq && target.kind.contains(kind),
None => name_eq,
}
})
});
if targets.is_empty() {
bail!(
"No Cargo targets matched by `package.metadata.maturin.targets`, please check your `Cargo.toml`"
);
} else {
let target_names = targets
.iter()
.map(|CompileTarget { target, .. }| target.name.as_str())
.collect::<Vec<_>>();
eprintln!(
"🎯 Found {} Cargo targets in `Cargo.toml`: {}",
targets.len(),
target_names.join(", ")
);
}
}
Ok(targets)
}
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")?;
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] => {
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!(
"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,
}
}
Ok(None)
}
fn is_generating_import_lib(cargo_metadata: &Metadata) -> Result<bool> {
let resolve = cargo_metadata
.resolve
.as_ref()
.context("Expected cargo to return metadata with resolve")?;
for &lib in PYO3_BINDING_CRATES.iter().rev() {
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] => {
let generate_import_lib = pyo3_crate
.features
.iter()
.any(|x| x == "generate-import-lib" || x == "generate-abi3-import-lib");
return Ok(generate_import_lib);
}
_ => continue,
}
}
Ok(false)
}
fn find_bindings(
deps: &HashMap<&str, &Node>,
packages: &HashMap<&str, &cargo_metadata::Package>,
) -> Option<(String, usize)> {
if deps.get("pyo3").is_some() {
let ver = &packages["pyo3"].version;
let minor =
pyo3_minimum_python_minor_version(ver.major, ver.minor).unwrap_or(MINIMUM_PYTHON_MINOR);
Some(("pyo3".to_string(), minor))
} else if deps.get("pyo3-ffi").is_some() {
let ver = &packages["pyo3-ffi"].version;
let minor = pyo3_ffi_minimum_python_minor_version(ver.major, ver.minor)
.unwrap_or(MINIMUM_PYTHON_MINOR);
Some(("pyo3-ffi".to_string(), minor))
} else if deps.contains_key("cpython") {
Some(("rust-cpython".to_string(), MINIMUM_PYTHON_MINOR))
} else if deps.contains_key("uniffi") {
Some(("uniffi".to_string(), MINIMUM_PYTHON_MINOR))
} else {
None
}
}
pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<BridgeModel> {
let resolve = cargo_metadata
.resolve
.as_ref()
.ok_or_else(|| format_err!("Expected to get a dependency graph from cargo"))?;
let deps: HashMap<&str, &Node> = resolve
.nodes
.iter()
.map(|node| (cargo_metadata[&node.id].name.as_ref(), node))
.collect();
let packages: HashMap<&str, &cargo_metadata::Package> = cargo_metadata
.packages
.iter()
.filter_map(|pkg| {
let name = &pkg.name;
if name == "pyo3" || name == "pyo3-ffi" || name == "cpython" || name == "uniffi" {
Some((name.as_ref(), pkg))
} else {
None
}
})
.collect();
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| {
kind != "example" && kind != "test" && kind != "bench" && kind != "custom-build"
})
})
.flat_map(|target| target.crate_types.iter())
.map(String::as_str)
.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_bindings(&deps, &packages).filter(|(bindings, _)| bindings != "uniffi");
BridgeModel::Bin(bindings)
} else {
if !deps.contains_key(bindings) {
bail!(
"The bindings crate {} was not found in the dependencies list",
bindings
);
}
BridgeModel::Bindings(bindings.to_string(), MINIMUM_PYTHON_MINOR)
}
} else if let Some((bindings, minor)) = find_bindings(&deps, &packages) {
if !targets.contains(&"cdylib") && targets.contains(&"bin") {
if bindings == "uniffi" {
BridgeModel::Bin(None)
} else {
BridgeModel::Bin(Some((bindings, minor)))
}
} else if bindings == "uniffi" {
BridgeModel::UniFfi
} else {
BridgeModel::Bindings(bindings, minor)
}
} else if targets.contains(&"cdylib") {
BridgeModel::Cffi
} else if targets.contains(&"bin") {
BridgeModel::Bin(find_bindings(&deps, &packages))
} else {
bail!("Couldn't detect the binding type; Please specify them with --bindings/-b")
};
if !(bridge.is_bindings("pyo3") || bridge.is_bindings("pyo3-ffi")) {
eprintln!("🔗 Found {bridge} bindings");
return Ok(bridge);
}
for &lib in PYO3_BINDING_CRATES.iter() {
if !bridge.is_bin() && bridge.is_bindings(lib) {
let pyo3_node = deps[lib];
if !pyo3_node.features.contains(&"extension-module".to_string()) {
let version = cargo_metadata[&pyo3_node.id].version.to_string();
eprintln!(
"⚠️ Warning: You're building a library without activating {lib}'s \
`extension-module` feature. \
See https://pyo3.rs/v{version}/building_and_distribution.html#linking"
);
}
return if let Some((major, minor)) = has_abi3(cargo_metadata)? {
eprintln!("🔗 Found {lib} bindings with abi3 support for Python ≥ {major}.{minor}");
Ok(BridgeModel::BindingsAbi3(major, minor))
} else {
eprintln!("🔗 Found {lib} bindings");
Ok(bridge)
};
}
}
Ok(bridge)
}
fn find_single_python_interpreter(
bridge: &BridgeModel,
interpreter: &[PathBuf],
target: &Target,
bridge_name: &str,
) -> Result<PythonInterpreter> {
let err_message = "Failed to find a python interpreter";
let executable = if interpreter.is_empty() {
target.get_python()
} else if interpreter.len() == 1 {
interpreter[0].clone()
} else {
bail!(
"You can only specify one python interpreter for {}",
bridge_name
);
};
let interpreter = PythonInterpreter::check_executable(executable, target, bridge)
.context(format_err!(err_message))?
.ok_or_else(|| format_err!(err_message))?;
Ok(interpreter)
}
fn find_interpreter(
bridge: &BridgeModel,
interpreter: &[PathBuf],
target: &Target,
requires_python: Option<&VersionSpecifiers>,
) -> Result<Vec<PythonInterpreter>> {
let mut found_interpreters = Vec::new();
if !interpreter.is_empty() {
let mut missing = Vec::new();
for interp in interpreter {
match PythonInterpreter::check_executable(interp.clone(), target, bridge) {
Ok(Some(interp)) => found_interpreters.push(interp),
_ => missing.push(interp.clone()),
}
}
if !missing.is_empty() {
let sysconfig_interps =
find_interpreter_in_sysconfig(&missing, target, requires_python)?;
found_interpreters.extend(sysconfig_interps);
}
} else {
found_interpreters = PythonInterpreter::find_all(target, bridge, requires_python)
.context("Finding python interpreters failed")?;
};
if found_interpreters.is_empty() {
if interpreter.is_empty() {
if let Some(requires_python) = requires_python {
bail!("Couldn't find any python interpreters with version {}. Please specify at least one with -i", requires_python);
} else {
bail!("Couldn't find any python interpreters. Please specify at least one with -i");
}
} else {
let interps_str = interpreter
.iter()
.map(|path| format!("'{}'", path.display()))
.collect::<Vec<_>>()
.join(", ");
bail!(
"Couldn't find any python interpreters from {}.",
interps_str
);
}
}
Ok(found_interpreters)
}
fn find_interpreter_in_host(
bridge: &BridgeModel,
interpreter: &[PathBuf],
target: &Target,
requires_python: Option<&VersionSpecifiers>,
) -> Result<Vec<PythonInterpreter>> {
let interpreters = if !interpreter.is_empty() {
PythonInterpreter::check_executables(interpreter, target, bridge)?
} else {
PythonInterpreter::find_all(target, bridge, requires_python)
.context("Finding python interpreters failed")?
};
if interpreters.is_empty() {
if let Some(requires_python) = requires_python {
bail!("Couldn't find any python interpreters with {}. Please specify at least one with -i", requires_python);
} else {
bail!("Couldn't find any python interpreters. Please specify at least one with -i");
}
}
Ok(interpreters)
}
fn find_interpreter_in_sysconfig(
interpreter: &[PathBuf],
target: &Target,
requires_python: Option<&VersionSpecifiers>,
) -> Result<Vec<PythonInterpreter>> {
if interpreter.is_empty() {
return Ok(PythonInterpreter::find_by_target(target, requires_python));
}
let mut interpreters = Vec::new();
for interp in interpreter {
let python = interp.display().to_string();
let (python_impl, python_ver) = if let Some(ver) = python.strip_prefix("pypy") {
(InterpreterKind::PyPy, ver.strip_prefix('-').unwrap_or(ver))
} else if let Some(ver) = python.strip_prefix("graalpy") {
(
InterpreterKind::GraalPy,
ver.strip_prefix('-').unwrap_or(ver),
)
} else if let Some(ver) = python.strip_prefix("python") {
(
InterpreterKind::CPython,
ver.strip_prefix('-').unwrap_or(ver),
)
} else if python
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
(InterpreterKind::CPython, &*python)
} else {
if std::path::Path::new(&python).is_file() {
bail!("Python interpreter should be a kind of interpreter (e.g. 'python3.8' or 'pypy3.9') when cross-compiling, got path to interpreter: {}", python);
} else {
bail!("Unsupported Python interpreter for cross-compilation: {}; supported interpreters are pypy, graalpy, and python (cpython)", python);
}
};
if python_ver.is_empty() {
continue;
}
let (ver_major, ver_minor) = python_ver
.split_once('.')
.context("Invalid python interpreter version")?;
let ver_major = ver_major.parse::<usize>().with_context(|| {
format!("Invalid python interpreter major version '{ver_major}', expect a digit")
})?;
let ver_minor = ver_minor.parse::<usize>().with_context(|| {
format!("Invalid python interpreter minor version '{ver_minor}', expect a digit")
})?;
let sysconfig = InterpreterConfig::lookup_one(target, python_impl, (ver_major, ver_minor))
.with_context(|| {
format!("Failed to find a {python_impl} {ver_major}.{ver_minor} interpreter in known sysconfig")
})?;
debug!(
"Found {} {}.{} in bundled sysconfig",
sysconfig.interpreter_kind, sysconfig.major, sysconfig.minor,
);
interpreters.push(PythonInterpreter::from_config(sysconfig.clone()));
}
Ok(interpreters)
}
pub(crate) fn extract_cargo_metadata_args(cargo_options: &CargoOptions) -> Result<Vec<String>> {
let mut cargo_metadata_extra_args = vec![];
if cargo_options.frozen {
cargo_metadata_extra_args.push("--frozen".to_string());
}
if cargo_options.locked {
cargo_metadata_extra_args.push("--locked".to_string());
}
if cargo_options.offline {
cargo_metadata_extra_args.push("--offline".to_string());
}
for feature in &cargo_options.features {
cargo_metadata_extra_args.push("--features".to_string());
cargo_metadata_extra_args.push(feature.clone());
}
if cargo_options.all_features {
cargo_metadata_extra_args.push("--all-features".to_string());
}
if cargo_options.no_default_features {
cargo_metadata_extra_args.push("--no-default-features".to_string());
}
for opt in &cargo_options.unstable_flags {
cargo_metadata_extra_args.push("-Z".to_string());
cargo_metadata_extra_args.push(opt.clone());
}
Ok(cargo_metadata_extra_args)
}
impl From<CargoOptions> for cargo_options::Rustc {
fn from(cargo: CargoOptions) -> Self {
cargo_options::Rustc {
common: cargo_options::CommonOptions {
quiet: cargo.quiet,
jobs: cargo.jobs,
profile: cargo.profile,
features: cargo.features,
all_features: cargo.all_features,
no_default_features: cargo.no_default_features,
target: match cargo.target {
Some(target) => vec![target],
None => Vec::new(),
},
target_dir: cargo.target_dir,
verbose: cargo.verbose,
color: cargo.color,
frozen: cargo.frozen,
locked: cargo.locked,
offline: cargo.offline,
config: cargo.config,
unstable_flags: cargo.unstable_flags,
timings: cargo.timings,
..Default::default()
},
manifest_path: cargo.manifest_path,
ignore_rust_version: cargo.ignore_rust_version,
future_incompat_report: cargo.future_incompat_report,
args: cargo.args,
..Default::default()
}
}
}
impl CargoOptions {
pub fn merge_with_pyproject_toml(&mut self, tool_maturin: ToolMaturin) -> Vec<&'static str> {
let mut args_from_pyproject = Vec::new();
if self.manifest_path.is_none() && tool_maturin.manifest_path.is_some() {
self.manifest_path = tool_maturin.manifest_path.clone();
args_from_pyproject.push("manifest-path");
}
if self.profile.is_none() && tool_maturin.profile.is_some() {
self.profile = tool_maturin.profile.clone();
args_from_pyproject.push("profile");
}
if let Some(features) = tool_maturin.features {
if self.features.is_empty() {
self.features = features;
args_from_pyproject.push("features");
}
}
if let Some(all_features) = tool_maturin.all_features {
if !self.all_features {
self.all_features = all_features;
args_from_pyproject.push("all-features");
}
}
if let Some(no_default_features) = tool_maturin.no_default_features {
if !self.no_default_features {
self.no_default_features = no_default_features;
args_from_pyproject.push("no-default-features");
}
}
if let Some(frozen) = tool_maturin.frozen {
if !self.frozen {
self.frozen = frozen;
args_from_pyproject.push("frozen");
}
}
if let Some(locked) = tool_maturin.locked {
if !self.locked {
self.locked = locked;
args_from_pyproject.push("locked");
}
}
if let Some(config) = tool_maturin.config {
if self.config.is_empty() {
self.config = config;
args_from_pyproject.push("config");
}
}
if let Some(unstable_flags) = tool_maturin.unstable_flags {
if self.unstable_flags.is_empty() {
self.unstable_flags = unstable_flags;
args_from_pyproject.push("unstable-flags");
}
}
args_from_pyproject
}
}
#[cfg(test)]
mod test {
use cargo_metadata::MetadataCommand;
use pretty_assertions::assert_eq;
use std::path::Path;
use super::*;
#[test]
fn test_find_bridge_pyo3() {
let pyo3_mixed = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/pyo3-mixed").join("Cargo.toml"))
.exec()
.unwrap();
assert!(matches!(
find_bridge(&pyo3_mixed, None),
Ok(BridgeModel::Bindings(..))
));
assert!(matches!(
find_bridge(&pyo3_mixed, Some("pyo3")),
Ok(BridgeModel::Bindings(..))
));
assert!(find_bridge(&pyo3_mixed, Some("rust-cpython")).is_err());
}
#[test]
fn test_find_bridge_pyo3_abi3() {
let pyo3_pure = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/pyo3-pure").join("Cargo.toml"))
.exec()
.unwrap();
assert!(matches!(
find_bridge(&pyo3_pure, None),
Ok(BridgeModel::BindingsAbi3(3, 7))
));
assert!(matches!(
find_bridge(&pyo3_pure, Some("pyo3")),
Ok(BridgeModel::BindingsAbi3(3, 7))
));
assert!(find_bridge(&pyo3_pure, Some("rust-cpython")).is_err());
}
#[test]
fn test_find_bridge_pyo3_feature() {
let pyo3_pure = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/pyo3-feature").join("Cargo.toml"))
.exec()
.unwrap();
assert!(find_bridge(&pyo3_pure, None).is_err());
let pyo3_pure = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/pyo3-feature").join("Cargo.toml"))
.other_options(vec!["--features=pyo3".to_string()])
.exec()
.unwrap();
assert!(matches!(
find_bridge(&pyo3_pure, None).unwrap(),
BridgeModel::Bindings(..)
));
}
#[test]
fn test_find_bridge_cffi() {
let cffi_pure = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/cffi-pure").join("Cargo.toml"))
.exec()
.unwrap();
assert_eq!(
find_bridge(&cffi_pure, Some("cffi")).unwrap(),
BridgeModel::Cffi
);
assert_eq!(find_bridge(&cffi_pure, None).unwrap(), BridgeModel::Cffi);
assert!(find_bridge(&cffi_pure, Some("rust-cpython")).is_err());
assert!(find_bridge(&cffi_pure, Some("pyo3")).is_err());
}
#[test]
fn test_find_bridge_bin() {
let hello_world = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/hello-world").join("Cargo.toml"))
.exec()
.unwrap();
assert_eq!(
find_bridge(&hello_world, Some("bin")).unwrap(),
BridgeModel::Bin(None)
);
assert_eq!(
find_bridge(&hello_world, None).unwrap(),
BridgeModel::Bin(None)
);
assert!(find_bridge(&hello_world, Some("rust-cpython")).is_err());
assert!(find_bridge(&hello_world, Some("pyo3")).is_err());
let pyo3_bin = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/pyo3-bin").join("Cargo.toml"))
.exec()
.unwrap();
assert!(matches!(
find_bridge(&pyo3_bin, Some("bin")).unwrap(),
BridgeModel::Bin(Some((..)))
));
assert!(matches!(
find_bridge(&pyo3_bin, None).unwrap(),
BridgeModel::Bin(Some(..))
));
}
#[test]
fn test_old_extra_feature_args() {
let cargo_extra_args = CargoOptions {
no_default_features: true,
features: vec!["a".to_string(), "c".to_string()],
target: Some("x86_64-unknown-linux-musl".to_string()),
..Default::default()
};
let cargo_metadata_extra_args = extract_cargo_metadata_args(&cargo_extra_args).unwrap();
assert_eq!(
cargo_metadata_extra_args,
vec![
"--features",
"a",
"--features",
"c",
"--no-default-features",
]
);
}
#[test]
fn test_extract_cargo_metadata_args() {
let args = CargoOptions {
locked: true,
features: vec!["my-feature".to_string(), "other-feature".to_string()],
target: Some("x86_64-unknown-linux-musl".to_string()),
unstable_flags: vec!["unstable-options".to_string()],
..Default::default()
};
let expected = vec![
"--locked",
"--features",
"my-feature",
"--features",
"other-feature",
"-Z",
"unstable-options",
];
assert_eq!(extract_cargo_metadata_args(&args).unwrap(), expected);
}
}