use crate::build_context::{BridgeModel, ProjectLayout};
use crate::BuildContext;
use crate::CargoToml;
use crate::Manylinux;
use crate::Metadata21;
use crate::PythonInterpreter;
use crate::Target;
use cargo_metadata::{Metadata, MetadataCommand, Node};
use failure::{bail, err_msg, format_err, Error, ResultExt};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use structopt::StructOpt;
#[derive(Debug, Serialize, Deserialize, StructOpt, Clone, Eq, PartialEq)]
#[serde(default)]
pub struct BuildOptions {
#[structopt(
long,
possible_values = &["1", "1-unchecked", "2010", "2010-unchecked", "off"],
case_insensitive = true,
default_value = "1"
)]
pub manylinux: Manylinux,
#[structopt(short, long)]
pub interpreter: Option<Vec<PathBuf>>,
#[structopt(short, long)]
pub bindings: Option<String>,
#[structopt(
short = "m",
long = "manifest-path",
parse(from_os_str),
default_value = "Cargo.toml",
name = "PATH"
)]
pub manifest_path: PathBuf,
#[structopt(short, long, parse(from_os_str))]
pub out: Option<PathBuf>,
#[structopt(long = "skip-auditwheel")]
pub skip_auditwheel: bool,
#[structopt(long, name = "TRIPLE")]
pub target: Option<String>,
#[structopt(long = "cargo-extra-args")]
pub cargo_extra_args: Vec<String>,
#[structopt(long = "rustc-extra-args")]
pub rustc_extra_args: Vec<String>,
}
impl Default for BuildOptions {
fn default() -> Self {
BuildOptions {
manylinux: Manylinux::Manylinux1,
interpreter: Some(vec![]),
bindings: None,
manifest_path: PathBuf::from("Cargo.toml"),
out: None,
skip_auditwheel: false,
target: None,
cargo_extra_args: Vec::new(),
rustc_extra_args: Vec::new(),
}
}
}
impl BuildOptions {
pub fn into_build_context(self, release: bool, strip: bool) -> Result<BuildContext, Error> {
let manifest_file = self
.manifest_path
.canonicalize()
.context(format_err!("Can't find {}", self.manifest_path.display()))?;
if !manifest_file.is_file() {
bail!(
"{} (resolved to {}) is not the path to a Cargo.toml",
self.manifest_path.display(),
manifest_file.display()
);
};
let cargo_toml = CargoToml::from_path(&manifest_file)?;
let manifest_dir = manifest_file.parent().unwrap();
let metadata21 = Metadata21::from_cargo_toml(&cargo_toml, &manifest_dir)
.context("Failed to parse Cargo.toml into python metadata")?;
let scripts = cargo_toml.scripts();
let module_name = cargo_toml
.lib
.clone()
.and_then(|lib| lib.name)
.unwrap_or_else(|| cargo_toml.package.name.clone())
.to_owned();
let project_layout = ProjectLayout::determine(manifest_dir, &module_name)?;
let target = Target::from_target_triple(self.target.clone())?;
let cargo_metadata = MetadataCommand::new()
.manifest_path(&self.manifest_path)
.exec()
.map_err(|e| format_err!("Cargo metadata failed: {}", e))?;
let wheel_dir = match self.out {
Some(ref dir) => dir.clone(),
None => PathBuf::from(&cargo_metadata.target_directory).join("wheels"),
};
let bridge = find_bridge(&cargo_metadata, self.bindings.as_ref().map(|x| &**x))?;
if bridge != BridgeModel::Bin && module_name.contains('-') {
bail!(
"The module name must not contains a minus \
(Make sure you have set an appropriate [lib] name in your Cargo.toml)"
);
}
let interpreter = match self.interpreter {
Some(ref interpreter) if interpreter.is_empty() => vec![],
Some(interpreter) => find_interpreter(&bridge, &interpreter, &target)?,
None => find_interpreter(&bridge, &[], &target)?,
};
let mut cargo_extra_args = split_extra_args(&self.cargo_extra_args)?;
if let Some(target) = self.target {
cargo_extra_args.extend_from_slice(&["--target".to_string(), target]);
}
let rustc_extra_args = split_extra_args(&self.rustc_extra_args)?;
let manylinux = if self.skip_auditwheel {
eprintln!("⚠ --skip-auditwheel is deprecated, use --manylinux=1-unchecked");
Manylinux::Manylinux1Unchecked
} else {
self.manylinux
};
Ok(BuildContext {
target,
bridge,
project_layout,
metadata21,
scripts,
module_name,
manifest_path: self.manifest_path,
out: wheel_dir,
release,
strip,
manylinux,
cargo_extra_args,
rustc_extra_args,
interpreter,
cargo_metadata,
})
}
}
pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<BridgeModel, Error> {
let nodes = cargo_metadata
.resolve
.clone()
.ok_or_else(|| format_err!("Expected to get a dependency graph from cargo"))?
.nodes;
let deps: HashMap<String, Node> = nodes
.iter()
.map(|node| (cargo_metadata[&node.id].name.clone(), node.clone()))
.collect();
if let Some(bindings) = bridge {
if bindings == "cffi" {
Ok(BridgeModel::Cffi)
} else if bindings == "bin" {
Ok(BridgeModel::Bin)
} else {
if !deps.contains_key(bindings) {
bail!(
"The bindings crate {} was not found in the dependencies list",
bindings
);
}
Ok(BridgeModel::Bindings(bindings.to_string()))
}
} else if let Some(node) = deps.get("pyo3") {
println!("🔗 Found pyo3 bindings");
if !node.features.contains(&"extension-module".to_string()) {
let version = cargo_metadata[&node.id].version.to_string();
println!(
"⚠ Warning: You're building a library without activating pyo3's \
`extension-module` feature. \
See https://pyo3.rs/{}/building-and-distribution.html#linking",
version
);
}
Ok(BridgeModel::Bindings("pyo3".to_string()))
} else if deps.contains_key("cpython") {
println!("🔗 Found rust-cpython bindings");
Ok(BridgeModel::Bindings("rust_cpython".to_string()))
} else {
bail!("Couldn't find any bindings; Please specify them with --bindings/-b")
}
}
pub fn find_interpreter(
bridge: &BridgeModel,
interpreter: &[PathBuf],
target: &Target,
) -> Result<Vec<PythonInterpreter>, Error> {
match bridge {
BridgeModel::Bindings(_) => {
let interpreter = 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)
.context("Finding python interpreters failed")?
};
if interpreter.is_empty() {
bail!("Couldn't find any python interpreters. Please specify at least one with -i");
}
println!(
"🐍 Found {}",
interpreter
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(", ")
);
Ok(interpreter)
}
BridgeModel::Cffi => {
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 cffi compilation");
};
let err_message = "Failed to find python interpreter for generating cffi bindings";
let interpreter = PythonInterpreter::check_executable(executable, &target, &bridge)
.context(err_msg(err_message))?
.ok_or_else(|| err_msg(err_message))?;
println!("🐍 Using {} to generate the cffi bindings", interpreter);
Ok(vec![interpreter])
}
BridgeModel::Bin => Ok(vec![]),
}
}
fn split_extra_args(given_args: &[String]) -> Result<Vec<String>, Error> {
let mut splitted_args = vec![];
for arg in given_args {
match shlex::split(&arg) {
Some(split) => splitted_args.extend(split),
None => {
bail!(
"Couldn't split argument from `--cargo-extra-args`: '{}'",
arg
);
}
}
}
Ok(splitted_args)
}
#[cfg(test)]
mod test {
use std::path::Path;
use super::*;
#[test]
fn test_find_bridge_pyo3() {
let pyo3_pure = MetadataCommand::new()
.manifest_path(&Path::new("test-crates/pyo3-pure").join("Cargo.toml"))
.exec()
.unwrap();
assert!(match find_bridge(&pyo3_pure, None).unwrap() {
BridgeModel::Bindings(_) => true,
_ => false,
});
assert!(match find_bridge(&pyo3_pure, Some("pyo3")).unwrap() {
BridgeModel::Bindings(_) => true,
_ => false,
});
assert!(find_bridge(&pyo3_pure, Some("rust-cpython")).is_err());
}
#[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!(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
);
assert!(find_bridge(&hello_world, None).is_err());
assert!(find_bridge(&hello_world, Some("rust-cpython")).is_err());
assert!(find_bridge(&hello_world, Some("pyo3")).is_err());
}
#[test]
fn test_argument_splitting() {
let mut options = BuildOptions::default();
options.cargo_extra_args.push("--features foo".to_string());
options.bindings = Some("bin".to_string());
let context = options.into_build_context(false, false).unwrap();
assert_eq!(context.cargo_extra_args, vec!["--features", "foo"])
}
}