use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
#[path = "build/download.rs"]
mod download;
#[cfg(feature = "source-build")]
const HOGD_REPO_URL: &str = "https://github.com/thruputchain/hogd.git";
const TEST_FFI_VERSION: &str = "0.42.0";
const DOCS_RS_ARCHIVE: &str = "libtest_ffi-linux-x86_64.tar.gz";
#[cfg(feature = "source-build")]
const THRUPUT_CLI_VERSION: &str = "0.37.0";
#[cfg(feature = "source-build")]
fn ensure_hogd_clone(
out_dir: &Path,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let expected_tag = format!("test-ffi-v{TEST_FFI_VERSION}");
if let Ok(hogd_src) = env::var("HOGD_SRC") {
let hogd_path = PathBuf::from(&hogd_src);
if hogd_path.exists() {
return Ok(hogd_path);
}
clone_hogd_repo(&hogd_path, &expected_tag)?;
return Ok(hogd_path);
}
let build_key: String =
Sha256::digest(out_dir.as_os_str().as_encoded_bytes())
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
let hogd_path = env::temp_dir()
.join("thruput-sdk-hogd")
.join(build_key)
.join(format!("test-ffi-v{TEST_FFI_VERSION}"));
if !hogd_path.exists() {
clone_hogd_repo(&hogd_path, &expected_tag)?;
}
Ok(hogd_path)
}
#[cfg(feature = "source-build")]
fn clone_hogd_repo(
target_dir: &Path,
tag: &str,
) -> Result<(), Box<dyn std::error::Error>> {
println!(
"cargo:warning=Cloning hogd repository to: {}",
target_dir.display()
);
if let Some(parent) = target_dir.parent() {
fs::create_dir_all(parent)?;
}
let output = Command::new("git")
.args([
"clone",
"--recurse-submodules",
"--branch",
tag,
HOGD_REPO_URL,
])
.arg(target_dir)
.output()?;
if !output.status.success() {
return Err(format!(
"Failed to clone hogd repository: {}\n\
Ensure git is installed and you have network access.",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
println!("cargo:warning=Successfully cloned hogd at tag: {tag}");
Ok(())
}
#[cfg(feature = "source-build")]
fn parse_version_from_header(
header_path: &Path,
) -> Result<String, Box<dyn std::error::Error>> {
let content = fs::read_to_string(header_path)?;
let mut major = None;
let mut minor = None;
let mut patch = None;
for line in content.lines() {
let line = line.trim();
if line.starts_with("#define TEST_FFI_VERSION_MAJOR") {
major = line
.split_whitespace()
.nth(2)
.and_then(|s| s.parse::<u32>().ok());
} else if line.starts_with("#define TEST_FFI_VERSION_MINOR") {
minor = line
.split_whitespace()
.nth(2)
.and_then(|s| s.parse::<u32>().ok());
} else if line.starts_with("#define TEST_FFI_VERSION_PATCH") {
patch = line
.split_whitespace()
.nth(2)
.and_then(|s| s.parse::<u32>().ok());
}
}
match (major, minor, patch) {
(Some(maj), Some(min), Some(pat)) => Ok(format!("{maj}.{min}.{pat}")),
_ => Err("Could not parse version from test_bindings.h".into()),
}
}
fn target_os() -> String {
env::var("CARGO_CFG_TARGET_OS").unwrap()
}
fn target_arch() -> String {
env::var("CARGO_CFG_TARGET_ARCH").unwrap()
}
fn shared_lib_prefix() -> &'static str {
match &target_os() as &str {
"windows" => "",
_ => "lib",
}
}
fn download_cache_dir() -> PathBuf {
let dir: PathBuf = env::var_os("HOGD_DOWNLOAD_CACHE_DIR").map_or_else(
|| {
PathBuf::from(env::var("OUT_DIR").unwrap())
.join("test_ffi_download_cache")
},
Into::into,
);
dir.join(format!("test-ffi-v{TEST_FFI_VERSION}"))
}
fn shared_lib_suffix() -> &'static str {
match &target_os() as &str {
"windows" => ".dll",
"macos" => ".dylib",
_ => ".so",
}
}
fn shared_lib_name() -> String {
let prefix = shared_lib_prefix();
let suffix = shared_lib_suffix();
format!("{prefix}test_ffi{suffix}")
}
fn emit_link_config(lib_dir: &Path) {
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=dylib=test_ffi");
println!("cargo:metadata=lib_dir={}", lib_dir.display());
}
fn find_prebuilt_bindings(
extract_dir: &Path,
out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let bindings_src = extract_dir.join("bindings.rs");
let bindings_dst = out_dir.join("bindings.rs");
if !bindings_src.exists() {
return Err(format!(
"bindings.rs not found in extracted tarball at: {}\n\
The prebuilt tarball may be from an older version that doesn't include bindings.\n\
Either:\n\
- Update to a newer prebuilt tarball that includes bindings.rs, or\n\
- Use HOGD_FFI_BUILD=1 to build from source (requires the `source-build` feature)",
bindings_src.display()
)
.into());
}
fs::copy(&bindings_src, &bindings_dst)?;
Ok(())
}
#[derive(Clone, Copy)]
enum Install {
Move,
Copy,
}
fn install_lib_in_deps(
lib_dir: &Path,
how: Install,
) -> Result<PathBuf, Box<dyn std::error::Error>> {
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
let deps_dir = out_dir
.ancestors()
.find(|p| p.file_name() == Some(std::ffi::OsStr::new("build")))
.and_then(|build| build.parent())
.ok_or_else(|| {
format!(
"OUT_DIR {} has no `build` ancestor; cannot locate <profile>/deps",
out_dir.display()
)
})?
.join("deps");
let lib_name = shared_lib_name();
let src = lib_dir.join(&lib_name);
let dst = deps_dir.join(&lib_name);
if !src.exists() {
return Err(
format!("Shared library not found at {}", src.display()).into()
);
}
fs::create_dir_all(&deps_dir).map_err(|e| {
format!("failed to create {}: {e}", deps_dir.display())
})?;
if is_same_file(&src, &dst) {
return Ok(deps_dir);
}
let installed = match how {
Install::Move => {
fs::rename(&src, &dst).or_else(|_| fs::copy(&src, &dst).map(drop))
}
Install::Copy => fs::copy(&src, &dst).map(drop),
};
installed.map_err(|e| {
format!(
"failed to install {} into {}: {e}",
src.display(),
deps_dir.display()
)
})?;
Ok(deps_dir)
}
fn is_same_file(a: &Path, b: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
match (fs::metadata(a), fs::metadata(b)) {
(Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
_ => false,
}
}
#[cfg(not(unix))]
{
matches!((a.canonicalize(), b.canonicalize()), (Ok(a), Ok(b)) if a == b)
}
}
enum BuildMethod {
Prebuilt,
CustomLib(String),
Source,
}
fn resolve_build_method() -> Result<BuildMethod, Box<dyn std::error::Error>> {
let prebuilt_supported = matches!(
(target_arch().as_str(), target_os().as_str()),
("x86_64" | "aarch64", "linux") | ("aarch64", "macos")
);
let build_from_source = env::var("HOGD_FFI_BUILD")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false);
let lib_dir = env::var("HOGD_FFI_LIB_DIR").ok();
if build_from_source && lib_dir.is_some() {
return Err(
"Multiple build methods specified. Please set only one of: \
HOGD_FFI_BUILD or HOGD_FFI_LIB_DIR"
.into(),
);
}
if let Some(dir) = lib_dir {
return Ok(BuildMethod::CustomLib(dir));
}
if build_from_source {
return Ok(BuildMethod::Source);
}
if prebuilt_supported {
return Ok(BuildMethod::Prebuilt);
}
let arch = target_arch();
let os = target_os();
Err(format!(
"No prebuilt library available for {arch}-{os}.\n\
Supported platforms: x86_64-linux, aarch64-linux, aarch64-macos\n\
Please set one of the following environment variables:\n\
- HOGD_FFI_BUILD=1 : Build from source using CMake\n\
- HOGD_FFI_LIB_DIR=<path> : Use prebuilt library from specified directory",
)
.into())
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
std::process::exit(1);
}
}
fn run() -> Result<(), Box<dyn std::error::Error>> {
let target = env::var("TARGET").unwrap();
if target.starts_with("riscv32") {
return Err(format!("thruput-sys does not build for {target}").into());
}
println!("cargo:rerun-if-env-changed=HOGD_FFI_BUILD");
println!("cargo:rerun-if-env-changed=HOGD_FFI_LIB_DIR");
println!("cargo:rerun-if-env-changed=HOGD_SRC");
println!("cargo:rerun-if-env-changed=NATIVE_EXEC_ALWAYS_CONFIGURE");
println!("cargo:rerun-if-env-changed=DOCS_RS");
println!("cargo:rustc-check-cfg=cfg(thruput_sys_docs_rs_archive)");
let out_dir = PathBuf::from(env::var("OUT_DIR")?);
if env::var_os("DOCS_RS").is_some() {
println!("cargo:rerun-if-changed=docs-rs/bindings.rs");
fs::copy("docs-rs/bindings.rs", out_dir.join("bindings.rs"))
.map_err(|e| format!("failed to copy docs-rs/bindings.rs: {e}"))?;
return Ok(());
}
match resolve_build_method()? {
BuildMethod::Prebuilt => {
run_prebuilt_download(&out_dir)?;
}
BuildMethod::CustomLib(dir) => {
let lib_path = PathBuf::from(&dir);
find_prebuilt_bindings(&lib_path, &out_dir)?;
use_prebuilt_lib(&dir)?;
}
BuildMethod::Source => {
#[cfg(feature = "source-build")]
run_build_from_source(&out_dir)?;
#[cfg(not(feature = "source-build"))]
return Err(
"HOGD_FFI_BUILD=1 requires the `source-build` feature so \
cmake and bindgen are available.\n\
Build with: cargo build -p thruput-sys --features source-build"
.into(),
);
}
}
Ok(())
}
fn run_prebuilt_download(
out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let lock_path = out_dir
.parent()
.unwrap()
.parent()
.unwrap()
.join("thruput-sys.fslock");
let mut lockfile = fslock::LockFile::open(&lock_path)
.expect("bug: could not open build lock file");
lockfile.lock_with_pid().expect(
"could not acquire build lock — another cargo build may be running",
);
let arch = target_arch();
let os = target_os();
let archive_name =
download_and_extract(out_dir, &arch, &os, TEST_FFI_VERSION)?;
if archive_name == DOCS_RS_ARCHIVE {
println!("cargo:rustc-cfg=thruput_sys_docs_rs_archive");
}
Ok(())
}
#[cfg(feature = "source-build")]
fn run_build_from_source(
out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:warning=Building from source (HOGD_FFI_BUILD=1)");
let hogd_path = ensure_hogd_clone(out_dir)?;
let hogd_src = hogd_path.to_str().unwrap();
println!("cargo:warning=HOGD_SRC is '{hogd_src}'");
println!("cargo:rerun-if-changed={hogd_src}");
println!("cargo:rerun-if-changed=.");
let native_exec_bindings = hogd_path.join("hogd/test_ffi/test_bindings.h");
let hogd_ffi_version = parse_version_from_header(&native_exec_bindings)?;
println!("cargo:warning=Parsed test_ffi version: {hogd_ffi_version}");
if hogd_ffi_version != TEST_FFI_VERSION {
return Err(format!(
"Version mismatch between hogd header and build.rs constant.\n\
Header version: {hogd_ffi_version}\n\
Constant version: {TEST_FFI_VERSION}\n\
Please update TEST_FFI_VERSION in crates/thruput-sys/build.rs to match the header.",
)
.into());
}
let bindings = bindgen::Builder::default()
.header(native_exec_bindings.to_str().unwrap())
.no_copy("FFIDataBuffer")
.generate()
.expect("Unable to generate bindings");
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("Couldn't write bindings!");
verify_hogd_version(hogd_src, &hogd_ffi_version)?;
fetch_hogd_rust_deps(&hogd_path, out_dir)?;
build_from_cmake(hogd_src, out_dir)?;
Ok(())
}
#[cfg(feature = "source-build")]
fn fetch_hogd_rust_deps(
hogd_path: &Path,
out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let contracts_dir = hogd_path.join("built_contracts");
if contracts_dir.exists() {
println!("cargo:warning=Pre-fetching built_contracts dependencies...");
let output = Command::new("cargo")
.args(["fetch", "-Zbuild-std"])
.env("CARGO_NET_GIT_FETCH_WITH_CLI", "true")
.current_dir(&contracts_dir)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"cargo fetch -Zbuild-std in built_contracts failed: {stderr}"
)
.into());
}
let output = Command::new("cargo")
.args([
"fetch",
"-Zbuild-std",
"--target",
"riscv32im-unknown-none-elf",
])
.env("CARGO_NET_GIT_FETCH_WITH_CLI", "true")
.current_dir(&contracts_dir)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!(
"cargo fetch -Zbuild-std --target \
riscv32im-unknown-none-elf in built_contracts failed: \
{stderr}"
)
.into());
}
} else {
println!(
"cargo:warning=built_contracts not found, skipping \
contracts pre-fetch"
);
}
let rust_dir = hogd_path.join("hogd/rust");
if rust_dir.exists() {
println!("cargo:warning=Pre-fetching hogd Rust dependencies...");
let output = Command::new("cargo")
.args(["fetch"])
.env("CARGO_NET_GIT_FETCH_WITH_CLI", "true")
.current_dir(&rust_dir)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(
format!("cargo fetch in hogd/rust failed: {stderr}").into()
);
}
} else {
println!(
"cargo:warning=hogd/rust not found, skipping \
pre-fetch"
);
}
let cli_root = out_dir.join("thruput-cli");
let cli_bin_path = cli_root.join("bin/thruput");
let expected_ver = format!("thruput {THRUPUT_CLI_VERSION}");
let needs_install = if cli_bin_path.exists() {
let output =
Command::new(&cli_bin_path).arg("--version").output().ok();
let installed_ver = output
.as_ref()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
let installed_release = installed_ver
.split_once(" (")
.map_or(installed_ver.as_str(), |(release, _)| release);
if installed_release == expected_ver {
println!(
"cargo:warning=thruput-cli {THRUPUT_CLI_VERSION} \
already installed, skipping",
);
false
} else {
println!(
"cargo:warning=thruput-cli version mismatch \
(have '{installed_ver}', want '{expected_ver}'), reinstalling",
);
true
}
} else {
true
};
if needs_install {
println!(
"cargo:warning=Installing thruput-cli to {}...",
cli_root.display()
);
let pkg = format!("thruput-cli@{THRUPUT_CLI_VERSION}");
let output = Command::new("cargo")
.args([
"install",
"--locked",
"--root",
cli_root.to_str().unwrap(),
"--registry",
"thruput",
&pkg,
])
.env(
"CARGO_REGISTRIES_THRUPUT_INDEX",
"sparse+https://crates.thruput.network/api/v1/crates/",
)
.env("CARGO_NET_GIT_FETCH_WITH_CLI", "true")
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(
format!("cargo install thruput-cli failed: {stderr}").into()
);
}
}
Ok(())
}
#[cfg(feature = "source-build")]
fn verify_hogd_version(
hogd_src: &str,
version: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let expected_tag = format!("test-ffi-v{version}");
let output = Command::new("git")
.args(["describe", "--tags", "--exact-match"])
.current_dir(hogd_src)
.output()?;
if !output.status.success() {
return Err(format!(
"hogd checkout is not at a release tag. Expected tag: {expected_tag}\n\
Please checkout the correct tag in the hogd checkout:\n\
cd {hogd_src} && git checkout {expected_tag}",
)
.into());
}
let current_tag = String::from_utf8(output.stdout)?.trim().to_string();
if current_tag != expected_tag {
return Err(format!(
"hogd checkout version mismatch.\n\
Expected tag: {expected_tag}\n\
Current tag: {current_tag}\n\
Please checkout the correct tag:\n\
cd {hogd_src} && git checkout {expected_tag}",
)
.into());
}
println!("cargo:warning=hogd version verified: {current_tag}");
Ok(())
}
fn use_prebuilt_lib(lib_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
let lib_path = PathBuf::from(lib_dir);
if !lib_path.exists() {
return Err(
format!("Library directory does not exist: {lib_dir}").into()
);
}
let expected_lib = shared_lib_name();
let lib_file = lib_path.join(&expected_lib);
if !lib_file.exists() {
let arch = target_arch();
let os = target_os();
return Err(format!(
"Could not find expected library file '{expected_lib}' \
for target '{arch}' os '{os}' in directory: {lib_dir}\n\
Please ensure the correct library file exists.",
)
.into());
}
let deps_dir = install_lib_in_deps(&lib_path, Install::Copy)?;
emit_link_config(&deps_dir);
println!(
"cargo:warning=Using prebuilt library from: {lib_dir} (found: {expected_lib}); installed in {}",
deps_dir.display(),
);
Ok(())
}
fn ensure_cached_download(
url: &str,
dest: &Path,
label: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let sum_path = dest.with_extension("sum");
let cache_valid = dest.exists()
&& sum_path.exists()
&& fs::read_to_string(&sum_path)
.map(|cached| cached == url)
.unwrap_or(false);
if cache_valid {
return Ok(());
}
let _ = fs::remove_file(dest);
let _ = fs::remove_file(&sum_path);
println!("cargo:warning=Downloading {label} from: {url}");
download::download_with_retry(
Path::new("curl"),
url,
dest,
TEST_FFI_VERSION,
3,
Duration::from_secs(5),
)
.map_err(|e| format!("{label} download failed: {e}"))?;
fs::write(&sum_path, url)
.map_err(|e| format!("failed to write {label} cache sum: {e}"))?;
Ok(())
}
fn verify_checksum(
archive_path: &Path,
checksum_path: &Path,
archive_bytes: &[u8],
) -> Result<(), Box<dyn std::error::Error>> {
if !checksum_path.exists() {
return Err(format!(
"Checksum file not found at {}; refusing to use an unverified \
archive.",
checksum_path.display()
)
.into());
}
let checksum_content = fs::read_to_string(checksum_path)?;
let expected = checksum_content
.split_whitespace()
.next()
.ok_or("Checksum file is empty")?;
let mut hasher = Sha256::new();
hasher.update(archive_bytes);
let actual: String = hasher
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect();
if actual != expected {
return Err(format!(
"Checksum verification failed!\n\
Expected: {expected}\n\
Actual: {actual}\n\
Archive: {}",
archive_path.display()
)
.into());
}
Ok(())
}
fn download_and_extract(
out_dir: &Path,
target_arch: &str,
target_os: &str,
version: &str,
) -> Result<&'static str, Box<dyn std::error::Error>> {
let archive_name = match (target_arch, target_os) {
("x86_64", "linux") => "libtest_ffi-linux-x86_64.tar.gz",
("aarch64", "linux") => "libtest_ffi-linux-arm64.tar.gz",
("aarch64", "macos") => "libtest_ffi-macos-arm64.tar.gz",
_ => {
return Err(format!(
"No prebuilt library available for {target_arch}-{target_os}.\n\
Supported platforms: x86_64-linux, aarch64-linux, aarch64-macos\n\
Please use HOGD_FFI_BUILD=1 to build from source instead.",
)
.into());
}
};
let download_url = format!(
"https://groundhog-public-release-artifacts-017820684264.s3.us-west-2.amazonaws.com/test-ffi/test-ffi-v{version}/{archive_name}",
);
let cache_dir = download_cache_dir();
fs::create_dir_all(&cache_dir)?;
let archive_path = cache_dir.join(archive_name);
ensure_cached_download(&download_url, &archive_path, "Archive")?;
let checksum_url = format!("{download_url}.sha256");
let checksum_path = cache_dir.join(format!("{archive_name}.sha256"));
ensure_cached_download(&checksum_url, &checksum_path, "Checksum")
.map_err(|e| format!("refusing to use an unverified archive: {e}"))?;
let archive_bytes = fs::read(&archive_path)?;
verify_checksum(&archive_path, &checksum_path, &archive_bytes)?;
let extract_dir = out_dir.join(format!("test_ffi_lib_{version}"));
fs::create_dir_all(&extract_dir)?;
let listing = Command::new("tar")
.arg("-tzf")
.arg(&archive_path)
.output()
.map_err(|e| format!("tar not found or failed to execute: {e}"))?;
if !listing.status.success() {
return Err(format!(
"tar failed to list {} (exit {:?})",
archive_path.display(),
listing.status.code()
)
.into());
}
let mut expected_entries = vec![
shared_lib_name(),
"test_bindings.h".into(),
"bindings.rs".into(),
];
expected_entries.sort();
let mut found_entries: Vec<String> =
String::from_utf8_lossy(&listing.stdout)
.lines()
.filter(|entry| *entry != "./")
.map(|entry| entry.strip_prefix("./").unwrap_or(entry).to_string())
.collect();
found_entries.sort();
if found_entries != expected_entries {
return Err(format!(
"archive {} does not contain exactly the expected files.\n\
Expected: {expected_entries:?}\n\
Found: {found_entries:?}",
archive_path.display()
)
.into());
}
let status = Command::new("tar")
.arg("-xzf")
.arg(&archive_path)
.arg("-C")
.arg(&extract_dir)
.status()
.map_err(|e| format!("tar not found or failed to execute: {e}"))?;
if !status.success() {
return Err(format!(
"tar failed to extract {} (exit {:?})",
archive_path.display(),
status.code()
)
.into());
}
let lib_found = extract_dir.join("libtest_ffi.so").exists()
|| extract_dir.join("libtest_ffi.dylib").exists()
|| extract_dir.join("test_ffi.dll").exists();
if !lib_found {
return Err(format!(
"Could not find libtest_ffi library in extracted archive at: {}",
extract_dir.display()
)
.into());
}
find_prebuilt_bindings(&extract_dir, out_dir)?;
let deps_dir = install_lib_in_deps(&extract_dir, Install::Move)?;
emit_link_config(&deps_dir);
Ok(archive_name)
}
#[cfg(feature = "source-build")]
fn build_from_cmake(
hogd_src: &str,
out_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut cfg = cmake::Config::new(hogd_src);
cfg.define("CMAKE_BUILD_TYPE", "Debug")
.define("CMAKE_VERBOSE_MAKEFILE", "ON")
.define("CMAKE_OSX_SYSROOT", "")
.define("ENABLE_TEST_FFI", "ON")
.define("CMAKE_C_COMPILER_LAUNCHER", "ccache")
.define("CMAKE_CXX_COMPILER_LAUNCHER", "ccache")
.define("ENABLE_TEST_CONTRACTS", "OFF");
cfg.always_configure(
env::var("NATIVE_EXEC_ALWAYS_CONFIGURE").is_ok_and(|s| !s.is_empty()),
);
let cli_bin = out_dir.join("thruput-cli/bin");
if cli_bin.exists() {
let path = env::var("PATH").unwrap_or_default();
cfg.env("PATH", format!("{}:{path}", cli_bin.display()));
}
cfg.build_target("test_ffi");
let dst = cfg.build();
let lib_dir = dst.join("build/hogd/test_ffi");
let deps_dir = install_lib_in_deps(&lib_dir, Install::Copy)?;
emit_link_config(&deps_dir);
println!(
"cargo:warning=Built library from source at: {}; installed in {}",
dst.display(),
deps_dir.display()
);
Ok(())
}