use anyhow::{bail, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::{CrateType, Message, Metadata, MetadataCommand, Package, Target};
use fs_err as fs;
use once_cell::sync::Lazy;
use std::{
collections::hash_map::DefaultHasher,
env,
env::consts::DLL_EXTENSION,
hash::{Hash, Hasher},
process::{Command, Stdio},
};
#[derive(Debug)]
pub struct CompileSource {
pub udl_path: Utf8PathBuf,
pub config_path: Option<Utf8PathBuf>,
}
static CARGO_METADATA: Lazy<Metadata> = Lazy::new(get_cargo_metadata);
static CARGO_BUILD_MESSAGES: Lazy<Vec<Message>> = Lazy::new(get_cargo_build_messages);
pub struct UniFFITestHelper {
crate_name: String,
cdylib: Utf8PathBuf,
}
impl UniFFITestHelper {
pub fn new(package_name: &str) -> Result<Self> {
let package = Self::find_package(package_name)?;
let (crate_name, cdylib) = Self::find_name_and_cdylib_path(&package)?;
Ok(Self { crate_name, cdylib })
}
fn find_package(name: &str) -> Result<Package> {
let matching: Vec<&Package> = CARGO_METADATA
.packages
.iter()
.filter(|p| p.name == name)
.collect();
match matching.len() {
1 => Ok(matching[0].clone()),
n => bail!("cargo metadata return {n} packages named {name}"),
}
}
fn find_name_and_cdylib_path(package: &Package) -> Result<(String, Utf8PathBuf)> {
let cdylib_targets: Vec<&Target> = package
.targets
.iter()
.filter(|t| t.crate_types.contains(&CrateType::CDyLib))
.collect();
let target = match cdylib_targets.len() {
1 => cdylib_targets[0],
n => bail!("Found {n} cdylib targets for {}", package.name),
};
let target_name = target.name.replace('-', "_");
let artifacts = CARGO_BUILD_MESSAGES
.iter()
.filter_map(|message| match message {
Message::CompilerArtifact(artifact) => {
if artifact.target == *target {
Some(artifact.clone())
} else {
None
}
}
_ => None,
});
let cdylib_files: Vec<Utf8PathBuf> = artifacts
.into_iter()
.flat_map(|artifact| {
artifact
.filenames
.into_iter()
.filter(|nm| matches!(nm.extension(), Some(DLL_EXTENSION)))
.collect::<Vec<Utf8PathBuf>>()
})
.collect();
match cdylib_files.len() {
1 => Ok((target_name, cdylib_files[0].to_owned())),
n => bail!("Found {n} cdylib files for {}", package.name),
}
}
pub fn create_out_dir(
&self,
temp_dir: impl AsRef<Utf8Path>,
script_path: impl AsRef<Utf8Path>,
) -> Result<Utf8PathBuf> {
let dirname = format!("{}-{}", self.crate_name, hash_path(script_path.as_ref()));
let out_dir = temp_dir.as_ref().join(dirname);
if out_dir.exists() {
fs::remove_dir_all(&out_dir)?;
}
fs::create_dir(&out_dir)?;
println!("Creating testing out_dir: {out_dir}");
Ok(out_dir)
}
pub fn copy_cdylib_to_out_dir(&self, out_dir: impl AsRef<Utf8Path>) -> Result<Utf8PathBuf> {
let path = self.cdylib_path()?;
let dest = out_dir.as_ref().join(path.file_name().unwrap());
fs::copy(&path, &dest)?;
Ok(dest)
}
pub fn cdylib_path(&self) -> Result<Utf8PathBuf> {
Ok(self.cdylib.clone())
}
pub fn crate_name(&self) -> &str {
&self.crate_name
}
pub fn cargo_metadata(&self) -> Metadata {
CARGO_METADATA.clone()
}
}
fn get_cargo_metadata() -> Metadata {
MetadataCommand::new()
.exec()
.expect("error running cargo metadata")
}
fn get_cargo_build_messages() -> Vec<Message> {
#[cfg(feature = "ffi-trace")]
let features_arg = "--features=ffi-trace";
#[cfg(not(feature = "ffi-trace"))]
let features_arg = "--features=";
let mut child = Command::new(env!("CARGO"))
.arg("test")
.arg(features_arg)
.arg("--no-run")
.arg("--message-format=json")
.stdout(Stdio::piped())
.spawn()
.expect("Error running cargo build");
let output = std::io::BufReader::new(child.stdout.take().unwrap());
let result = Message::parse_stream(output)
.map(|m| m.expect("Error parsing cargo build messages"))
.collect();
child.wait().expect("failed to wait for child");
result
}
fn hash_path(path: &Utf8Path) -> String {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
format!("{:x}", hasher.finish())
}