use std::collections::BTreeSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
const VENDORED_DIRS: &[&str] = &["include", "lib", "cmake"];
const VENDORED_FILES: &[&str] = &["CMakeLists.txt", "LICENSE"];
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let submodule_dir = manifest_dir.join("fizzy");
let vendored_dir = manifest_dir.join("fizzy-vendored");
let shim = manifest_dir.join("shim").join("compat_shim.hpp");
println!("cargo:rerun-if-changed=wrapper.h");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", shim.display());
if submodule_dir.join("CMakeLists.txt").exists() {
println!("cargo:rerun-if-changed={}", submodule_dir.display());
sync_vendored(&submodule_dir, &vendored_dir);
}
if !vendored_dir.join("CMakeLists.txt").exists() {
panic!(
"Fizzy sources are missing: neither the submodule `{}` nor the vendored \
copy `{}` is present.\n\
Run `git submodule update --init --recursive` and rebuild.",
submodule_dir.display(),
vendored_dir.display()
);
}
generate_bindings(&vendored_dir.join("include"), &out_dir);
if env::var_os("DOCS_RS").is_some() {
return;
}
build_and_link(&vendored_dir, &shim);
}
fn sync_vendored(submodule: &Path, vendored: &Path) {
let mut kept: BTreeSet<PathBuf> = BTreeSet::new();
for dir in VENDORED_DIRS {
mirror_dir(
&submodule.join(dir),
&vendored.join(dir),
Path::new(dir),
&mut kept,
);
}
for file in VENDORED_FILES {
let src = submodule.join(file);
if src.exists() {
copy_if_different(&src, &vendored.join(file));
kept.insert(PathBuf::from(file));
}
}
prune_extraneous(vendored, vendored, &kept);
}
fn mirror_dir(src: &Path, dst: &Path, rel: &Path, kept: &mut BTreeSet<PathBuf>) {
let entries =
fs::read_dir(src).unwrap_or_else(|e| panic!("failed to read `{}`: {e}", src.display()));
for entry in entries {
let entry = entry.unwrap();
let child_src = entry.path();
let child_rel = rel.join(entry.file_name());
let child_dst = dst.join(entry.file_name());
if child_src.is_dir() {
mirror_dir(&child_src, &child_dst, &child_rel, kept);
} else {
copy_if_different(&child_src, &child_dst);
kept.insert(child_rel);
}
}
}
fn copy_if_different(src: &Path, dst: &Path) {
let src_bytes =
fs::read(src).unwrap_or_else(|e| panic!("failed to read `{}`: {e}", src.display()));
if fs::read(dst).is_ok_and(|dst_bytes| dst_bytes == src_bytes) {
return;
}
if let Some(parent) = dst.parent() {
fs::create_dir_all(parent)
.unwrap_or_else(|e| panic!("failed to create `{}`: {e}", parent.display()));
}
fs::write(dst, &src_bytes)
.unwrap_or_else(|e| panic!("failed to write `{}`: {e}", dst.display()));
}
fn prune_extraneous(root: &Path, dir: &Path, kept: &BTreeSet<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries {
let path = entry.unwrap().path();
if path.is_dir() {
prune_extraneous(root, &path, kept);
let _ = fs::remove_dir(&path); } else {
let rel = path.strip_prefix(root).unwrap();
if !kept.contains(rel) {
let _ = fs::remove_file(&path);
}
}
}
}
fn build_and_link(source_dir: &Path, shim: &Path) {
let dst = cmake::Config::new(source_dir)
.define("FIZZY_TESTING", "OFF")
.define("FIZZY_WASI", "OFF")
.define("HUNTER_ENABLED", "OFF")
.cxxflag("-include")
.cxxflag(shim.to_str().expect("shim path is not valid UTF-8"))
.build_target("fizzy")
.build();
println!(
"cargo:rustc-link-search=native={}",
dst.join("build").join("lib").display()
);
println!("cargo:rustc-link-lib=static=fizzy");
println!("cargo:rustc-link-lib=dylib={}", cpp_stdlib());
}
fn generate_bindings(include_dir: &Path, out_dir: &Path) {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.clang_arg(format!("-I{}", include_dir.display()))
.allowlist_function("fizzy_.*")
.allowlist_type("Fizzy.*")
.allowlist_var("Fizzy.*")
.use_core()
.ctypes_prefix("::core::ffi")
.prepend_enum_name(false)
.derive_default(true)
.derive_debug(true)
.generate()
.expect("failed to generate Fizzy bindings");
bindings
.write_to_file(out_dir.join("bindings.rs"))
.expect("failed to write Fizzy bindings");
}
fn cpp_stdlib() -> &'static str {
let target = env::var("TARGET").unwrap_or_default();
if target.contains("apple") || target.contains("freebsd") || target.contains("openbsd") {
"c++"
} else {
"stdc++"
}
}