use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use cargo_metadata::{Metadata, MetadataCommand};
use failure::{bail, err_msg, format_err, Error, ResultExt};
use serde::{Deserialize, Serialize};
use structopt::StructOpt;
use toml;
use crate::build_context::BridgeModel;
use crate::cargo_toml::CargoTomlMetadata;
use crate::cargo_toml::CargoTomlMetadataPyo3Pack;
use crate::BuildContext;
use crate::CargoToml;
use crate::Manylinux;
use crate::Metadata21;
use crate::PythonInterpreter;
use crate::Target;
#[derive(Debug, Serialize, Deserialize, StructOpt, Clone, Eq, PartialEq)]
#[serde(default)]
pub struct BuildOptions {
#[structopt(
long,
raw(
possible_values = r#"&["1", "1-unchecked", "2010", "2010-unchecked", "off"]"#,
case_insensitive = "true",
default_value = r#""1""#
)
)]
pub manylinux: Manylinux,
#[structopt(short, long)]
pub interpreter: Vec<String>,
#[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: 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 !self.manifest_path.is_file() {
bail!("{} must be a path to a Cargo.toml", manifest_file.display());
};
let contents = fs::read_to_string(&manifest_file).context(format!(
"Can't read Cargo.toml at {}",
manifest_file.display(),
))?;
let cargo_toml: CargoToml = toml::from_str(&contents).context(format!(
"Failed to parse Cargo.toml at {}",
manifest_file.display()
))?;
let manifest_dir = manifest_file.parent().unwrap().to_path_buf();
let metadata21 = Metadata21::from_cargo_toml(&cargo_toml, &manifest_dir)
.context("Failed to parse Cargo.toml into python metadata")?;
let scripts = match cargo_toml.package.metadata {
Some(CargoTomlMetadata {
pyo3_pack:
Some(CargoTomlMetadataPyo3Pack {
scripts: Some(ref scripts),
..
}),
}) => scripts.clone(),
_ => HashMap::new(),
};
let module_name = cargo_toml
.lib
.clone()
.and_then(|lib| lib.name)
.unwrap_or_else(|| cargo_toml.package.name.clone())
.to_owned();
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 = find_interpreter(&bridge, &self.interpreter, &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,
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 deps: HashSet<String> = cargo_metadata
.resolve
.clone()
.unwrap()
.nodes
.iter()
.map(|node| cargo_metadata[&node.id].name.clone())
.collect();
if let Some(bindings) = bridge {
if bindings == "cffi" {
Ok(BridgeModel::Cffi)
} else if bindings == "bin" {
Ok(BridgeModel::Bin)
} else {
if !deps.contains(bindings) {
bail!(
"The bindings crate {} was not found in the dependencies list",
bindings
);
}
Ok(BridgeModel::Bindings(bindings.to_string()))
}
} else if deps.contains("pyo3") {
println!("🔗 Found pyo3 bindings");
Ok(BridgeModel::Bindings("pyo3".to_string()))
} else if deps.contains("cpython") {
println!("🔗 Found rust-cpython bindings");
Ok(BridgeModel::Bindings("rust_cpython".to_string()))
} else {
bail!("Couldn't find any bindings; Please specify them with -b")
}
}
pub fn find_interpreter(
bridge: &BridgeModel,
interpreter: &[String],
target: &Target,
) -> Result<Vec<PythonInterpreter>, Error> {
Ok(match bridge {
BridgeModel::Bindings(_) => {
let interpreter = if !interpreter.is_empty() {
PythonInterpreter::check_executables(&interpreter, &target)?
} else {
PythonInterpreter::find_all(&target)?
};
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(", ")
);
interpreter
}
BridgeModel::Cffi => {
let executable = if interpreter.is_empty() {
target.get_python()
} else if interpreter.len() == 1 {
PathBuf::from(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)
.context(err_msg(err_message))?
.ok_or_else(|| err_msg(err_message))?;
println!("🐍 Using {} to generate the cffi bindings", interpreter);
vec![interpreter]
}
BridgeModel::Bin => 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 get_fourtytwo = MetadataCommand::new()
.manifest_path(&Path::new("get-fourtytwo").join("Cargo.toml"))
.exec()
.unwrap();
assert!(match find_bridge(&get_fourtytwo, None).unwrap() {
BridgeModel::Bindings(_) => true,
_ => false,
});
assert!(match find_bridge(&get_fourtytwo, Some("pyo3")).unwrap() {
BridgeModel::Bindings(_) => true,
_ => false,
});
assert!(find_bridge(&get_fourtytwo, Some("rust-cpython")).is_err());
}
#[test]
fn test_find_bridge_cffi() {
let points = MetadataCommand::new()
.manifest_path(&Path::new("points").join("Cargo.toml"))
.exec()
.unwrap();
assert_eq!(
find_bridge(&points, Some("cffi")).unwrap(),
BridgeModel::Cffi
);
assert!(find_bridge(&points, Some("rust-cpython")).is_err());
assert!(find_bridge(&points, Some("pyo3")).is_err());
}
#[test]
fn test_find_bridge_bin() {
let hello_world = MetadataCommand::new()
.manifest_path(&Path::new("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"])
}
}