use crate::build_context::BridgeModel;
use crate::python_interpreter::InterpreterKind;
use crate::BuildContext;
use crate::PythonInterpreter;
use anyhow::{anyhow, bail, Context, Result};
use fat_macho::FatWriter;
use fs_err::{self as fs, File};
use std::collections::HashMap;
use std::env;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::str;
pub fn compile(
context: &BuildContext,
python_interpreter: Option<&PythonInterpreter>,
bindings_crate: &BridgeModel,
) -> Result<HashMap<String, PathBuf>> {
if context.target.is_macos() && context.universal2 {
compile_universal2(context, python_interpreter, bindings_crate)
} else {
compile_target(context, python_interpreter, bindings_crate)
}
}
fn compile_universal2(
context: &BuildContext,
python_interpreter: Option<&PythonInterpreter>,
bindings_crate: &BridgeModel,
) -> Result<HashMap<String, PathBuf>> {
let build_type = match bindings_crate {
BridgeModel::Bin => "bin",
_ => "cdylib",
};
let mut aarch64_context = context.clone();
aarch64_context.cargo_extra_args.extend(vec![
"--target".to_string(),
"aarch64-apple-darwin".to_string(),
]);
let aarch64_artifact = compile_target(&aarch64_context, python_interpreter, bindings_crate)
.context("Failed to build a aarch64 library through cargo")?
.get(build_type)
.cloned()
.ok_or_else(|| {
if build_type == "cdylib" {
anyhow!(
"Cargo didn't build an aarch64 cdylib. Did you miss crate-type = [\"cdylib\"] \
in the lib section of your Cargo.toml?",
)
} else {
anyhow!("Cargo didn't build an aarch64 bin.")
}
})?;
let mut x86_64_context = context.clone();
x86_64_context.cargo_extra_args.extend(vec![
"--target".to_string(),
"x86_64-apple-darwin".to_string(),
]);
let x86_64_artifact = compile_target(&x86_64_context, python_interpreter, bindings_crate)
.context("Failed to build a x86_64 library through cargo")?
.get(build_type)
.cloned()
.ok_or_else(|| {
if build_type == "cdylib" {
anyhow!(
"Cargo didn't build a x86_64 cdylib. Did you miss crate-type = [\"cdylib\"] \
in the lib section of your Cargo.toml?",
)
} else {
anyhow!("Cargo didn't build a x86_64 bin.")
}
})?;
let output_path = aarch64_artifact
.display()
.to_string()
.replace("aarch64-apple-darwin/", "");
let mut writer = FatWriter::new();
let aarch64_file = fs::read(aarch64_artifact)?;
let x86_64_file = fs::read(x86_64_artifact)?;
writer
.add(aarch64_file)
.map_err(|e| anyhow!("Failed to add aarch64 cdylib: {:?}", e))?;
writer
.add(x86_64_file)
.map_err(|e| anyhow!("Failed to add x86_64 cdylib: {:?}", e))?;
writer
.write_to_file(&output_path)
.map_err(|e| anyhow!("Failed to create universal cdylib: {:?}", e))?;
let mut result = HashMap::new();
result.insert(build_type.to_string(), PathBuf::from(output_path));
Ok(result)
}
fn compile_target(
context: &BuildContext,
python_interpreter: Option<&PythonInterpreter>,
bindings_crate: &BridgeModel,
) -> Result<HashMap<String, PathBuf>> {
let mut shared_args = vec!["--manifest-path", context.manifest_path.to_str().unwrap()];
shared_args.extend(context.cargo_extra_args.iter().map(String::as_str));
if context.release {
shared_args.push("--release");
}
let mut rustc_args: Vec<&str> = context
.rustc_extra_args
.iter()
.map(String::as_str)
.collect();
let mut rust_flags = env::var_os("RUSTFLAGS").unwrap_or_default();
match bindings_crate {
BridgeModel::Bin => shared_args.push("--bins"),
BridgeModel::Cffi | BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3(_, _) => {
shared_args.push("--lib");
if context.target.is_musl_target() {
rust_flags.push(" -C target-feature=-crt-static");
}
}
}
let module_name = &context.module_name;
let so_filename = match python_interpreter {
Some(python_interpreter) => python_interpreter.get_library_name(module_name),
None => {
format!("{base}.abi3.so", base = module_name)
}
};
let macos_dylib_install_name = format!("link-args=-Wl,-install_name,@rpath/{}", so_filename);
if context.target.is_macos() {
if let BridgeModel::Bindings(_) | BridgeModel::BindingsAbi3(_, _) = bindings_crate {
let mac_args = &[
"-C",
"link-arg=-undefined",
"-C",
"link-arg=dynamic_lookup",
"-C",
&macos_dylib_install_name,
];
rustc_args.extend(mac_args);
}
}
if context.strip {
rustc_args.extend(&["-C", "link-arg=-s"]);
}
let pythonxy_lib_folder;
if let BridgeModel::BindingsAbi3(_, _) = bindings_crate {
if context.target.is_windows() {
let python_interpreter = python_interpreter
.expect("Must have a python interpreter for building abi3 on windows");
pythonxy_lib_folder = format!("native={}", python_interpreter.libs_dir.display());
rustc_args.extend(&["-L", &pythonxy_lib_folder]);
}
}
let cargo_args = vec!["rustc", "--message-format", "json"];
let build_args: Vec<_> = cargo_args
.iter()
.chain(&shared_args)
.chain(&["--"])
.chain(&rustc_args)
.collect();
let command_str = build_args
.iter()
.fold("cargo".to_string(), |acc, x| acc + " " + x);
let mut let_binding = Command::new("cargo");
let build_command = let_binding
.env("RUSTFLAGS", rust_flags)
.args(&build_args)
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
if let BridgeModel::BindingsAbi3(_, _) = bindings_crate {
let is_pypy = python_interpreter
.map(|p| p.interpreter_kind == InterpreterKind::PyPy)
.unwrap_or(false);
if !is_pypy {
build_command.env("PYO3_NO_PYTHON", "1");
}
}
if let Some(python_interpreter) = python_interpreter {
if python_interpreter.executable != PathBuf::new() {
if bindings_crate.is_bindings("pyo3") {
build_command.env("PYO3_PYTHON", &python_interpreter.executable);
}
build_command.env("PYTHON_SYS_EXECUTABLE", &python_interpreter.executable);
}
}
let mut cargo_build = build_command.spawn().context("Failed to run cargo")?;
let mut artifacts = HashMap::new();
let stream = cargo_build
.stdout
.take()
.expect("Cargo build should have a stdout");
for message in cargo_metadata::Message::parse_stream(BufReader::new(stream)) {
match message.context("Failed to parse message coming from cargo")? {
cargo_metadata::Message::CompilerArtifact(artifact) => {
let package_in_metadata = context
.cargo_metadata
.packages
.iter()
.find(|package| package.id == artifact.package_id);
let crate_name = match package_in_metadata {
Some(package) => &package.name,
None => {
println!(
"⚠️ Warning: The package {} wasn't listed in `cargo metadata`",
artifact.package_id
);
continue;
}
};
if crate_name == &context.crate_name {
let tuples = artifact
.target
.crate_types
.into_iter()
.zip(artifact.filenames);
for (crate_type, filename) in tuples {
artifacts.insert(crate_type, filename.into());
}
}
}
cargo_metadata::Message::CompilerMessage(msg) => {
println!("{}", msg.message);
}
_ => (),
}
}
let status = cargo_build
.wait()
.expect("Failed to wait on cargo child process");
if !status.success() {
bail!(
r#"Cargo build finished with "{}": `{}`"#,
status,
command_str
)
}
Ok(artifacts)
}
pub fn warn_missing_py_init(artifact: &Path, module_name: &str) -> Result<()> {
let py_init = format!("PyInit_{}", module_name);
let mut fd = File::open(&artifact)?;
let mut buffer = Vec::new();
fd.read_to_end(&mut buffer)?;
let mut found = false;
match goblin::Object::parse(&buffer)? {
goblin::Object::Elf(elf) => {
for dyn_sym in elf.dynsyms.iter() {
if py_init == elf.dynstrtab[dyn_sym.st_name] {
found = true;
break;
}
}
}
goblin::Object::Mach(mach) => {
match mach {
goblin::mach::Mach::Binary(macho) => {
for sym in macho.exports()? {
let sym_name = sym.name;
if py_init == sym_name.strip_prefix('_').unwrap_or(&sym_name) {
found = true;
break;
}
}
if !found {
for sym in macho.symbols() {
let (sym_name, _) = sym?;
if py_init == sym_name.strip_prefix('_').unwrap_or(sym_name) {
found = true;
break;
}
}
}
}
goblin::mach::Mach::Fat(_) => {
found = true
}
}
}
goblin::Object::PE(pe) => {
for sym in &pe.exports {
if let Some(sym_name) = sym.name {
if py_init == sym_name {
found = true;
break;
}
}
}
}
_ => {
found = true
}
}
if !found {
println!(
"⚠️ Warning: Couldn't find the symbol `{}` in the native library. \
Python will fail to import this module. \
If you're using pyo3, check that `#[pymodule]` uses `{}` as module name",
py_init, module_name
)
}
Ok(())
}