use std::io::Read;
use std::path::{Path, PathBuf};
#[allow(dead_code)] mod embed_registry {
include!("src/vector/embed_registry.rs");
}
fn emit_build_stamp() {
use std::process::Command;
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=.git/index");
let sha = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".to_string());
let dirty = Command::new("git")
.args(["status", "--porcelain"])
.output()
.ok()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
let sha = if dirty { format!("{sha}-dirty") } else { sha };
println!("cargo:rustc-env=NORNIR_BUILD_SHA={sha}");
let epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
println!("cargo:rustc-env=NORNIR_BUILD_EPOCH={epoch}");
}
fn main() {
emit_build_stamp();
println!("cargo:rerun-if-changed=proto/nornir.proto");
println!("cargo:rerun-if-changed=build.rs");
if std::env::var_os("CARGO_FEATURE_EMBED_TRACT").is_some()
|| std::env::var_os("CARGO_FEATURE_EMBED_ORT").is_some()
{
fetch_model();
}
let want_server = std::env::var_os("CARGO_FEATURE_SERVER").is_some();
let want_mcp = std::env::var_os("CARGO_FEATURE_MCP").is_some();
let want_viz = std::env::var_os("CARGO_FEATURE_VIZ").is_some();
if !(want_server || want_mcp || want_viz) {
return;
}
let fds = protox::compile(["proto/nornir.proto"], ["proto"])
.expect("protox: compile nornir.proto");
tonic_build::configure()
.build_server(want_server)
.build_client(want_mcp || want_server || want_viz)
.compile_fds(fds)
.expect("tonic-build: codegen");
}
fn fetch_model() {
println!("cargo:rerun-if-env-changed=NORNIR_EMBED_MODEL");
println!("cargo:rerun-if-env-changed=NORNIR_MODEL_CACHE");
let model = embed_registry::selected().unwrap_or_else(|e| panic!("{e}"));
let files: &[(&str, &str)] = &[
(model.onnx_path, "model.onnx"),
(model.tokenizer_path, "tokenizer.json"),
];
let cache = model_cache_dir(model.cache_subdir);
std::fs::create_dir_all(&cache).expect("create model cache dir");
let skip = std::env::var_os("NORNIR_SKIP_MODEL_FETCH").is_some();
for (remote, local) in files {
let dest = cache.join(local);
if dest.exists() {
continue;
}
if skip {
panic!(
"NORNIR_SKIP_MODEL_FETCH set but {} is missing",
dest.display()
);
}
let url = format!(
"https://huggingface.co/{}/resolve/main/{remote}?download=true",
model.hf_repo
);
eprintln!(
"nornir: fetching {remote} for embed feature (model `{}`) …",
model.id
);
download(&url, &dest);
}
let weights_sha = sha256_file(&cache.join("model.onnx"));
let tokenizer_sha = sha256_file(&cache.join("tokenizer.json"));
println!("cargo:rustc-env=NORNIR_MODEL_DIR={}", cache.display());
println!("cargo:rustc-env=NORNIR_MODEL_WEIGHTS_SHA={weights_sha}");
println!("cargo:rustc-env=NORNIR_MODEL_TOKENIZER_SHA={tokenizer_sha}");
}
fn model_cache_dir(subdir: &str) -> PathBuf {
if let Some(dir) = std::env::var_os("NORNIR_MODEL_CACHE") {
return PathBuf::from(dir);
}
let base = std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR")));
base.join(".cache/nornir/models").join(subdir)
}
fn download(url: &str, dest: &Path) {
let resp = ureq::get(url)
.call()
.unwrap_or_else(|e| panic!("download {url}: {e}"));
let mut reader = resp.into_reader();
let tmp = dest.with_extension("part");
let mut out = std::fs::File::create(&tmp)
.unwrap_or_else(|e| panic!("create {}: {e}", tmp.display()));
let mut buf = [0u8; 1 << 16];
loop {
let n = reader
.read(&mut buf)
.unwrap_or_else(|e| panic!("read {url}: {e}"));
if n == 0 {
break;
}
use std::io::Write;
out.write_all(&buf[..n])
.unwrap_or_else(|e| panic!("write {}: {e}", tmp.display()));
}
drop(out);
std::fs::rename(&tmp, dest)
.unwrap_or_else(|e| panic!("rename {} -> {}: {e}", tmp.display(), dest.display()));
}
fn sha256_file(path: &Path) -> String {
use sha2::{Digest, Sha256};
let mut f = std::fs::File::open(path)
.unwrap_or_else(|e| panic!("open {}: {e}", path.display()));
let mut h = Sha256::new();
let mut buf = [0u8; 1 << 16];
loop {
let n = f.read(&mut buf).expect("read for hashing");
if n == 0 {
break;
}
h.update(&buf[..n]);
}
let digest = h.finalize();
let mut s = String::with_capacity(64);
const HEX: &[u8; 16] = b"0123456789abcdef";
for &b in digest.iter() {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}