use crate::auditwheel::PlatformTag;
use crate::build_context::BridgeModel;
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, Metadata21, PythonInterpreter, Target};
use anyhow::{bail, format_err, Context, Result};
use cargo_metadata::{Metadata, Node};
use regex::Regex;
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")]
pub jobs: Option<usize>,
#[arg(long, value_name = "PROFILE-NAME")]
pub profile: Option<String>,
#[arg(short = 'F', long, action = clap::ArgAction::Append)]
pub features: Vec<String>,
#[arg(long)]
pub all_features: bool,
#[arg(long)]
pub no_default_features: bool,
#[arg(long, value_name = "TRIPLE", env = "CARGO_BUILD_TARGET")]
pub target: Option<String>,
#[arg(long, value_name = "DIRECTORY")]
pub target_dir: Option<PathBuf>,
#[arg(short = 'm', long, value_name = "PATH")]
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)]
pub frozen: bool,
#[arg(long)]
pub locked: bool,
#[arg(long)]
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
)]
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,
#[arg(long)]
pub zig: bool,
#[arg(long)]
pub universal2: 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,
min_python_minor: Option<usize>,
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,
min_python_minor,
)?;
let host_python = &host_interpreters[0];
println!("🐍 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 abi_tag =
soabi.and_then(|abi| abi.split('-').nth(1).map(ToString::to_string));
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 {
None
}
})
.context("unsupported Python interpreter")?;
interpreters.push(PythonInterpreter {
config: InterpreterConfig {
major,
minor,
interpreter_kind,
abiflags,
ext_suffix: ext_suffix.to_string(),
abi_tag,
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implmentation_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");
}
interpreters =
find_interpreter_in_sysconfig(interpreter, target, min_python_minor)?;
}
} else if binding_name.starts_with("pyo3") {
interpreters = find_interpreter(bridge, interpreter, target, min_python_minor)?;
} else {
interpreters =
find_interpreter_in_host(bridge, interpreter, target, min_python_minor)?;
}
let interpreters_str = interpreters
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(", ");
println!("🐍 Found {interpreters_str}");
Ok(interpreters)
}
BridgeModel::Cffi => {
let interpreter =
find_single_python_interpreter(bridge, interpreter, target, "cffi")?;
println!("🐍 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,
Some(*minor as usize),
)
.unwrap_or_default();
if env::var_os("PYO3_CROSS_LIB_DIR").is_some() {
println!("⚠️ 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(),
abi_tag: None,
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implmentation_name: "cpython".to_string(),
soabi: None,
}])
} else if let Some(interp) = interpreters.get(0) {
println!("🐍 Using {interp} to generate to link bindings (With abi3, an interpreter is only required on windows)");
Ok(interpreters)
} else if generate_import_lib {
println!("🐍 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(),
abi_tag: None,
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implmentation_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,
Some(*minor as usize),
)
.or_else(|err| {
let interps = find_interpreter_in_sysconfig(
interpreter,
target,
Some(*minor as usize),
)
.unwrap_or_default();
if interps.is_empty() && !self.interpreter.is_empty() {
Err(err)
} else {
Ok(interps)
}
})?;
println!("🐍 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(),
abi_tag: None,
pointer_width: None,
},
executable: PathBuf::new(),
platform: None,
runnable: false,
implmentation_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,
min_python_minor,
)?)
}
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,
metadata21,
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() && module_name.contains('-') {
bail!(
"The module name must not contain a minus `-` \
(Make sure you have set an appropriate [lib] name or \
[package.metadata.maturin] name in your Cargo.toml)"
);
}
let mut target_triple = self.target.clone();
let mut universal2 = self.universal2;
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) if target_triple.is_none() => {
target_triple = Some("x86_64-apple-darwin".to_string())
}
(false, true) if target_triple.is_none() => {
target_triple = Some("aarch64-apple-darwin".to_string())
}
_ => {}
}
};
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,
get_min_python_minor(&metadata21),
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![PathBuf::from("python3")],
}
} else {
vec![PathBuf::from("python3")]
}
} else {
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() {
let compatibility = pyproject
.and_then(|x| {
if x.compatibility().is_some() {
pyproject_toml_maturin_options.push("compatibility");
}
x.compatibility()
})
.or(if self.zig {
if target.is_musl_target() {
Some(PlatformTag::Musllinux { x: 1, y: 2 })
} else {
Some(target.get_minimum_manylinux_tag())
}
} else {
if target.is_musl_target() && !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.");
}
}
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 crate_name = cargo_toml.package.name;
Ok(BuildContext {
target,
bridge,
project_layout,
pyproject_toml_path,
pyproject_toml,
metadata21,
crate_name,
module_name,
manifest_path: cargo_toml_path,
target_dir,
out: wheel_dir,
release,
strip,
skip_auditwheel,
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_target() {
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 get_min_python_minor(metadata21: &Metadata21) -> Option<usize> {
if let Some(requires_python) = &metadata21.requires_python {
let regex = Regex::new(r#">=3\.(\d+)(?:\.\d)?"#).unwrap();
if let Some(captures) = regex.captures(requires_python) {
let min_python_minor = captures[1]
.parse::<usize>()
.expect("Regex must only match usize");
Some(min_python_minor)
} else {
println!(
"⚠️ Couldn't parse the value of requires-python, \
not taking it into account when searching for python interpreter. \
Note: Only `>=3.x.y` is currently supported."
);
None
}
} else {
None
}
}
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")) {
println!("🔗 Found {bridge} bindings");
}
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)? {
println!("🔗 Found {lib} bindings with abi3 support for Python ≥ {major}.{minor}");
Ok(BridgeModel::BindingsAbi3(major, minor))
} else {
println!("🔗 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,
min_python_minor: Option<usize>,
) -> Result<Vec<PythonInterpreter>> {
let mut 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)) => interpreters.push(interp),
_ => missing.push(interp.clone()),
}
}
if !missing.is_empty() {
let sysconfig_interps =
find_interpreter_in_sysconfig(&missing, target, min_python_minor)?;
interpreters.extend(sysconfig_interps);
}
} else {
interpreters = PythonInterpreter::find_all(target, bridge, min_python_minor)
.context("Finding python interpreters failed")?;
};
if interpreters.is_empty() {
if let Some(minor) = min_python_minor {
bail!("Couldn't find any python interpreters with version >= 3.{}. Please specify at least one with -i", minor);
} else {
bail!("Couldn't find any python interpreters. Please specify at least one with -i");
}
}
Ok(interpreters)
}
fn find_interpreter_in_host(
bridge: &BridgeModel,
interpreter: &[PathBuf],
target: &Target,
min_python_minor: Option<usize>,
) -> Result<Vec<PythonInterpreter>> {
let interpreters = if !interpreter.is_empty() {
PythonInterpreter::check_executables(interpreter, target, bridge)
.context("The given list of python interpreters is invalid")?
} else {
PythonInterpreter::find_all(target, bridge, min_python_minor)
.context("Finding python interpreters failed")?
};
if interpreters.is_empty() {
if let Some(minor) = min_python_minor {
bail!("Couldn't find any python interpreters with version >= 3.{}. Please specify at least one with -i", minor);
} 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,
min_python_minor: Option<usize>,
) -> Result<Vec<PythonInterpreter>> {
if interpreter.is_empty() {
return Ok(PythonInterpreter::find_by_target(target, min_python_minor));
}
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("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 {
bail!("Unsupported Python interpreter: {}", python);
};
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(
target.target_os(),
target.target_arch(),
python_impl,
(ver_major, ver_minor),
)
.with_context(|| {
format!("Failed to find a {python_impl} {ver_major}.{ver_minor} interpreter")
})?;
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,
manifest_path: cargo.manifest_path,
ignore_rust_version: cargo.ignore_rust_version,
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()
},
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);
}
#[test]
fn test_get_min_python_minor() {
use crate::CargoToml;
let manifest_path = "test-crates/pyo3-pure/Cargo.toml";
let cargo_toml = CargoToml::from_path(manifest_path).unwrap();
let cargo_metadata = MetadataCommand::new()
.manifest_path(manifest_path)
.exec()
.unwrap();
let metadata21 =
Metadata21::from_cargo_toml(&cargo_toml, "test-crates/pyo3-pure", &cargo_metadata)
.unwrap();
assert_eq!(get_min_python_minor(&metadata21), None);
}
}