use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
const CORE_VERSION: &str = "2.8.0";
const CORE_COMMIT: &str = "d746dc6";
const CORE_WARNS: &str = "-Wall -Wextra -Wstrict-prototypes -Wno-unused-parameter";
const CORE_PRIVATE_HEADERS: &[&str] = &[
"lang/eval.h",
"lang/internal.h",
"ops/ops.h",
"store/serde.h",
"core/runtime.h",
];
const INTERNAL_FNS: &[&str] = &[
"ray_eval",
"ray_update_fn",
"ray_insert_fn",
"ray_upsert_fn",
"ray_read_csv_fn",
"ray_write_csv_fn",
"ray_set_splayed_fn",
"ray_get_splayed_fn",
"ray_get_parted_fn",
"ray_lazy_materialize",
"ray_ser",
"ray_de",
"ray_error_msg",
];
fn main() {
let core_is_vendored = env::var_os("RAYFORCE_SRC").is_none();
let core_src = core_src_dir();
let header_src = core_src.join("include/rayforce.h");
assert!(
header_src.exists(),
"rayforce core header not found at {}.\n\
If this is a git checkout, the vendored core submodule is not \
initialized — run `git submodule update --init --recursive`.\n\
To build against a different core, point RAYFORCE_SRC at it.",
header_src.display()
);
let core = if core_is_vendored {
stage_core(&core_src)
} else {
core_src.clone()
};
let include = core.join("include");
sanitize_libclang_path();
build_core_lib(&core, core_is_vendored);
let q_src = q_src_dir();
let q_c = q_src.join("q.c");
assert!(
q_c.exists(),
"rayforce-q client not found at {}.\n\
If this is a git checkout, the vendored submodule is not initialized \
— run `git submodule update --init --recursive`.\n\
To build against a different checkout, point RAYFORCE_Q_SRC at it.",
q_c.display()
);
cc::Build::new()
.file(&q_c)
.include(&q_src)
.include(&include)
.include(core.join("src"))
.warnings(false)
.compile("rayforce_q");
println!("cargo:rerun-if-changed={}", q_c.display());
println!("cargo:rerun-if-changed={}", q_src.join("q.h").display());
println!("cargo:rustc-link-search=native={}", core.display());
println!("cargo:rustc-link-lib=static=rayforce");
println!("cargo:rustc-link-lib=dylib=m");
if cfg!(target_os = "linux") {
println!("cargo:rustc-link-lib=dylib=pthread");
}
println!("cargo:root={}", core.display());
let mut builder = bindgen::Builder::default()
.header(include.join("rayforce.h").display().to_string())
.clang_arg(format!("-I{}", include.display()))
.clang_arg(format!("-I{}", core.join("src").display()))
.clang_arg("-D_Atomic(T)=T")
.allowlist_file(".*/include/rayforce\\.h")
.opaque_type("ray_runtime_s")
.layout_tests(true)
.derive_debug(false)
.generate_comments(false)
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
for header in CORE_PRIVATE_HEADERS {
builder = builder.header(core.join("src").join(header).display().to_string());
}
for func in INTERNAL_FNS {
builder = builder.allowlist_function(func);
}
builder
.generate()
.expect("failed to generate rayforce bindings")
.write_to_file(out_dir().join("bindings.rs"))
.expect("failed to write bindings.rs");
println!("cargo:rerun-if-changed=build.rs");
}
fn core_src_dir() -> PathBuf {
println!("cargo:rerun-if-env-changed=RAYFORCE_SRC");
let src = match env::var("RAYFORCE_SRC") {
Ok(p) => PathBuf::from(p),
Err(_) => vendored("rayforce"),
};
println!("cargo:rerun-if-changed={}", src.join("Makefile").display());
println!("cargo:rerun-if-changed={}", src.join("include").display());
println!("cargo:rerun-if-changed={}", src.join("src").display());
src
}
fn q_src_dir() -> PathBuf {
println!("cargo:rerun-if-env-changed=RAYFORCE_Q_SRC");
match env::var("RAYFORCE_Q_SRC") {
Ok(p) => PathBuf::from(p),
Err(_) => vendored("rayforce-q"),
}
}
fn vendored(name: &str) -> PathBuf {
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is always set"))
.join("vendor")
.join(name)
}
fn out_dir() -> PathBuf {
PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is always set"))
}
fn stage_core(src: &Path) -> PathBuf {
let dst = out_dir().join("core");
let mut staged = HashSet::new();
copy_if_stale(&src.join("Makefile"), &dst.join("Makefile"), &mut staged);
mirror(&src.join("include"), &dst.join("include"), &mut staged);
mirror(&src.join("src"), &dst.join("src"), &mut staged);
prune_stale(&dst, &staged);
dst
}
fn mirror(src: &Path, dst: &Path, staged: &mut HashSet<PathBuf>) {
let entries =
fs::read_dir(src).unwrap_or_else(|e| panic!("failed to read {}: {e}", src.display()));
for entry in entries.flatten() {
let from = entry.path();
let to = dst.join(entry.file_name());
if from.is_dir() {
mirror(&from, &to, staged);
} else if is_source(&from) {
copy_if_stale(&from, &to, staged);
}
}
}
fn is_source(p: &Path) -> bool {
matches!(p.extension().and_then(|e| e.to_str()), Some("c" | "h"))
}
fn copy_if_stale(from: &Path, to: &Path, staged: &mut HashSet<PathBuf>) {
staged.insert(to.to_path_buf());
if is_current(from, to) {
return;
}
let parent = to.parent().expect("staged paths always have a parent");
fs::create_dir_all(parent)
.unwrap_or_else(|e| panic!("failed to create {}: {e}", parent.display()));
fs::copy(from, to)
.unwrap_or_else(|e| panic!("failed to copy {} to {}: {e}", from.display(), to.display()));
}
fn is_current(from: &Path, to: &Path) -> bool {
let (Ok(f), Ok(t)) = (from.metadata(), to.metadata()) else {
return false;
};
match (f.modified(), t.modified()) {
(Ok(fm), Ok(tm)) => tm >= fm && f.len() == t.len(),
_ => false,
}
}
fn prune_stale(dst: &Path, staged: &HashSet<PathBuf>) {
for root in [dst.join("src"), dst.join("include")] {
walk(&root, &mut |path| {
if is_source(path) && !staged.contains(path) {
let _ = fs::remove_file(path);
}
});
}
}
fn walk(root: &Path, visit: &mut dyn FnMut(&Path)) {
let mut dirs = vec![root.to_path_buf()];
while let Some(dir) = dirs.pop() {
let Ok(entries) = fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
dirs.push(path);
} else {
visit(&path);
}
}
}
}
fn invalidate_on_stamp_change(core: &Path, stamp: &str) {
let marker = core.join(".stamp");
if fs::read_to_string(&marker).is_ok_and(|current| current == stamp) {
return;
}
walk(&core.join("src"), &mut |path| {
if path.extension().is_some_and(|e| e == "o") {
let _ = fs::remove_file(path);
}
});
let _ = fs::remove_file(core.join("librayforce.a"));
fs::write(&marker, stamp)
.unwrap_or_else(|e| panic!("failed to write {}: {e}", marker.display()));
}
fn sanitize_libclang_path() {
println!("cargo:rerun-if-env-changed=LIBCLANG_PATH");
let Ok(p) = env::var("LIBCLANG_PATH") else {
return;
};
let dir = Path::new(&p);
let has_libclang = fs::read_dir(dir).is_ok_and(|entries| {
entries.flatten().any(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
name.starts_with("libclang")
&& (name.contains(".so") || name.contains(".dylib") || name.contains(".dll"))
})
});
if !has_libclang {
println!(
"cargo:warning=LIBCLANG_PATH ({p}) contains no libclang; ignoring it \
so bindgen can auto-detect the system libclang."
);
env::remove_var("LIBCLANG_PATH");
}
}
#[derive(PartialEq, Eq, Clone, Copy)]
enum Flavour {
Release,
Debug,
}
fn core_flavour() -> Flavour {
println!("cargo:rerun-if-env-changed=RAYFORCE_CORE_DEBUG");
match env::var("RAYFORCE_CORE_DEBUG") {
Ok(v) if !v.is_empty() && v != "0" => Flavour::Debug,
_ => Flavour::Release,
}
}
fn build_core_lib(core: &Path, stamp_version: bool) {
let jobs = env::var("NUM_JOBS").unwrap_or_else(|_| "1".to_string());
let mut defs = vec![format!("WARNS={CORE_WARNS}")];
if core_flavour() == Flavour::Debug {
defs.push(
"RELEASE_CFLAGS=-fPIC $(WARNS) -std=$(STD) -g -O0 \
-march=$(RAY_MARCH) -DDEBUG -fno-omit-frame-pointer"
.to_string(),
);
}
if stamp_version {
defs.push(format!("RAY_VERSION={CORE_VERSION}"));
defs.push(format!("GIT_HASH={CORE_COMMIT}"));
}
invalidate_on_stamp_change(core, &defs.join(" "));
let status = Command::new("make")
.arg("lib")
.arg(format!("-j{jobs}"))
.args(&defs)
.current_dir(core)
.status()
.expect("failed to invoke `make` to build librayforce.a");
assert!(
status.success(),
"`make lib` failed in {} (exit {:?})",
core.display(),
status.code()
);
assert!(
core.join("librayforce.a").exists(),
"make lib succeeded but librayforce.a is missing in {}",
core.display()
);
}