use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
const GITHUB_REPO: &str = "qntx/xmtp";
fn main() {
println!("cargo:rerun-if-env-changed=XMTP_FFI_DIR");
println!("cargo:rerun-if-env-changed=XMTP_FFI_VERSION");
println!("cargo:rerun-if-env-changed=XMTP_UPDATE_BINDINGS");
println!("cargo:rerun-if-env-changed=DOCS_RS");
if env::var("DOCS_RS").is_ok() {
return;
}
let target = env::var("TARGET").expect("TARGET not set");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
if let Ok(ffi_dir) = env::var("XMTP_FFI_DIR") {
let ffi_path = PathBuf::from(&ffi_dir);
println!("cargo:warning=Using local FFI directory: {ffi_dir}");
println!("cargo:rustc-link-search=native={ffi_dir}");
#[cfg(feature = "regenerate")]
{
let header_path = find_header(&ffi_path);
println!("cargo:rerun-if-changed={}", header_path.display());
generate_bindings(&header_path, &out_dir);
}
let _ = ffi_path;
} else {
let version = env::var("XMTP_FFI_VERSION")
.unwrap_or_else(|_| env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set"));
let lib_dir = out_dir.join("lib");
let lib_file = lib_dir.join(lib_filename(&target));
if !lib_file.exists() {
download_and_extract(&version, &target, &lib_dir);
}
println!("cargo:rustc-link-search=native={}", lib_dir.display());
#[cfg(feature = "regenerate")]
{
let header_path = lib_dir.join("xmtp_ffi.h");
assert!(
header_path.exists(),
"Header file not found: {}",
header_path.display()
);
println!("cargo:rerun-if-changed={}", header_path.display());
generate_bindings(&header_path, &out_dir);
}
}
link_native_lib(&target);
link_system_libs(&target);
}
fn lib_filename(target: &str) -> &'static str {
if target.contains("windows") {
"xmtp_ffi.lib"
} else {
"libxmtp_ffi.a"
}
}
fn link_native_lib(target: &str) {
let _ = target;
println!("cargo:rustc-link-lib=static=xmtp_ffi");
}
fn link_system_libs(target: &str) {
if target.contains("linux") {
for lib in ["pthread", "dl", "m", "gcc_s", "stdc++"] {
println!("cargo:rustc-link-lib=dylib={lib}");
}
} else if target.contains("apple") {
for framework in ["Security", "CoreFoundation", "SystemConfiguration"] {
println!("cargo:rustc-link-lib=framework={framework}");
}
println!("cargo:rustc-link-lib=dylib=c++");
} else if target.contains("windows") {
for lib in [
"ws2_32", "bcrypt", "ntdll", "userenv", "crypt32", "secur32", "ncrypt", "user32",
] {
println!("cargo:rustc-link-lib=dylib={lib}");
}
}
}
fn download_and_extract(version: &str, target: &str, dest: &Path) {
let is_windows = target.contains("windows");
let ext = if is_windows { "zip" } else { "tar.gz" };
let url = format!(
"https://github.com/{GITHUB_REPO}/releases/download/ffi-v{version}/xmtp-ffi-{target}.{ext}"
);
eprintln!("Downloading {url}");
let resp = ureq::get(&url)
.call()
.unwrap_or_else(|e| panic!("Failed to download FFI library from {url}: {e}"));
fs::create_dir_all(dest).expect("Failed to create output directory");
let body = resp.into_body().into_reader();
if is_windows {
extract_zip(body, dest);
} else {
extract_tar_gz(body, dest);
}
let lib = dest.join(lib_filename(target));
assert!(
lib.exists(),
"Expected library file not found after extraction: {}",
lib.display()
);
}
fn extract_tar_gz(reader: impl io::Read, dest: &Path) {
let decoder = flate2::read::GzDecoder::new(reader);
let mut archive = tar::Archive::new(decoder);
archive
.unpack(dest)
.expect("Failed to extract tar.gz archive");
}
fn extract_zip(reader: impl io::Read, dest: &Path) {
let tmp = dest.join("__download.zip");
{
let mut file = fs::File::create(&tmp).expect("Failed to create temp zip file");
let mut reader = reader;
io::copy(&mut reader, &mut file).expect("Failed to write zip data");
}
let file = fs::File::open(&tmp).expect("Failed to open temp zip file");
let mut archive = zip::ZipArchive::new(file).expect("Failed to read zip archive");
archive
.extract(dest)
.expect("Failed to extract zip archive");
let _ = fs::remove_file(&tmp);
}
#[cfg(feature = "regenerate")]
fn find_header(ffi_dir: &Path) -> PathBuf {
let candidates = [
ffi_dir.join("include").join("xmtp_ffi.h"),
ffi_dir.join("xmtp_ffi.h"),
ffi_dir
.parent()
.and_then(|p| p.parent())
.map(|p| p.join("include").join("xmtp_ffi.h"))
.unwrap_or_default(),
];
for c in &candidates {
if c.exists() {
return c.clone();
}
}
panic!(
"Cannot find xmtp_ffi.h near XMTP_FFI_DIR={}\nSearched: {:?}",
ffi_dir.display(),
candidates
);
}
#[cfg(feature = "regenerate")]
fn generate_bindings(header: &Path, out_dir: &Path) {
let cleaned = preprocess_header(header, out_dir);
let bindings = bindgen::Builder::default()
.header(cleaned.to_str().expect("path is not valid UTF-8"))
.clang_arg("-xc++")
.use_core()
.allowlist_function("xmtp_.*")
.allowlist_type("Xmtp.*")
.allowlist_var("XMTP_.*")
.derive_debug(true)
.derive_default(true)
.derive_eq(true)
.default_enum_style(bindgen::EnumVariation::Rust {
non_exhaustive: true,
})
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("bindgen failed to generate bindings from xmtp_ffi.h");
let out_file = out_dir.join("bindings.rs");
bindings
.write_to_file(&out_file)
.expect("Failed to write bindings.rs");
if env::var("XMTP_UPDATE_BINDINGS").is_ok() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
let committed = manifest_dir.join("src").join("bindings.rs");
fs::copy(&out_file, &committed).expect("Failed to copy bindings.rs to src/");
println!(
"cargo:warning=Updated committed bindings: {}",
committed.display()
);
}
}
#[cfg(feature = "regenerate")]
fn preprocess_header(header: &Path, out_dir: &Path) -> PathBuf {
let content = fs::read_to_string(header).expect("Failed to read header");
let cleaned: String = content
.lines()
.filter(|line| {
let trimmed = line.trim();
!(trimmed.starts_with("typedef int32_t Xmtp") && trimmed.ends_with(';'))
})
.collect::<Vec<_>>()
.join("\n");
let out = out_dir.join("xmtp_ffi_cleaned.h");
fs::write(&out, cleaned).expect("Failed to write preprocessed header");
out
}