#![allow(clippy::needless_range_loop)]
use std::env;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
const MODEL_SHA256: &str = "afdb6f1a0e45b715d0bb9b11772f032c399babd23bfc31fed1c170afc848bdb1";
const MODEL_LEN: u64 = 22_972_370;
const MODEL_URL: &str =
"https://huggingface.co/Xenova/all-MiniLM-L6-v2/resolve/main/onnx/model_quantized.onnx";
const IN_TREE_REL: &str = "assets/all-MiniLM-L6-v2/onnx/model_quantized.onnx";
fn main() -> Result<(), String> {
let out = resolve_model().map_err(|e| {
format!(
"could not obtain the ONNX model weights: {e}\n\
Expected the file in-tree at `{IN_TREE_REL}` (dev/CI checkout), or a \
network reachable to download it from `{MODEL_URL}` (build from the \
published crate). Set EMBEDMIND_MODEL_ONNX to a pre-fetched copy to \
build fully offline."
)
})?;
println!("cargo:rustc-env=EMBEDMIND_MODEL_ONNX={}", out.display());
Ok(())
}
fn resolve_model() -> Result<PathBuf, String> {
if let Ok(explicit) = env::var("EMBEDMIND_MODEL_ONNX") {
let path = PathBuf::from(&explicit);
println!("cargo:rerun-if-changed={explicit}");
verify(&path)?;
return Ok(path);
}
println!("cargo:rerun-if-env-changed=EMBEDMIND_MODEL_ONNX");
let manifest_dir = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").map_err(|_| "CARGO_MANIFEST_DIR unset".to_string())?,
);
let in_tree = manifest_dir.join(IN_TREE_REL);
println!("cargo:rerun-if-changed={}", in_tree.display());
if in_tree.is_file() {
verify(&in_tree)?;
return Ok(in_tree);
}
let cached = cache_path()?;
if cached.is_file() && verify(&cached).is_ok() {
return Ok(cached);
}
download_to(&cached)?;
verify(&cached)?;
Ok(cached)
}
fn cache_path() -> Result<PathBuf, String> {
let dir = if let Some(cargo_home) = cargo_home() {
cargo_home.join("embedmind").join("models")
} else {
PathBuf::from(env::var("OUT_DIR").map_err(|_| "OUT_DIR unset".to_string())?)
.join("embedmind-models")
};
fs::create_dir_all(&dir).map_err(|e| format!("cannot create cache dir {dir:?}: {e}"))?;
Ok(dir.join(format!("all-MiniLM-L6-v2-{}.onnx", &MODEL_SHA256[..16])))
}
fn cargo_home() -> Option<PathBuf> {
if let Ok(h) = env::var("CARGO_HOME") {
return Some(PathBuf::from(h));
}
let home = env::var("HOME").or_else(|_| env::var("USERPROFILE")).ok()?;
Some(PathBuf::from(home).join(".cargo"))
}
fn verify(path: &Path) -> Result<(), String> {
let meta = fs::metadata(path).map_err(|e| format!("cannot stat {path:?}: {e}"))?;
if meta.len() != MODEL_LEN {
return Err(format!(
"{path:?} is {} bytes, expected {MODEL_LEN} — refusing to embed a truncated model",
meta.len()
));
}
let mut file = fs::File::open(path).map_err(|e| format!("cannot open {path:?}: {e}"))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 64 * 1024];
loop {
let n = file
.read(&mut buf)
.map_err(|e| format!("read error on {path:?}: {e}"))?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let got = hasher.hex();
if got != MODEL_SHA256 {
return Err(format!(
"{path:?} SHA-256 {got} != expected {MODEL_SHA256} — refusing to embed a \
model with an unexpected checksum"
));
}
Ok(())
}
fn download_to(dest: &Path) -> Result<(), String> {
let tmp = dest.with_extension("part");
let status = std::process::Command::new("curl")
.args([
"--fail",
"--location",
"--silent",
"--show-error",
"--retry",
"3",
"--output",
])
.arg(&tmp)
.arg(MODEL_URL)
.status()
.map_err(|e| {
format!(
"failed to run `curl` to download the model: {e}. Install curl, or set \
EMBEDMIND_MODEL_ONNX to a pre-fetched copy."
)
})?;
if !status.success() {
let _ = fs::remove_file(&tmp);
return Err(format!("curl exited with {status} downloading {MODEL_URL}"));
}
fs::rename(&tmp, dest).map_err(|e| format!("cannot move downloaded model into place: {e}"))?;
Ok(())
}
struct Sha256 {
state: [u32; 8],
len: u64,
buf: [u8; 64],
buf_len: usize,
}
impl Sha256 {
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
];
fn new() -> Self {
Sha256 {
state: [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
],
len: 0,
buf: [0u8; 64],
buf_len: 0,
}
}
fn update(&mut self, mut data: &[u8]) {
self.len = self.len.wrapping_add(data.len() as u64);
if self.buf_len > 0 {
let take = (64 - self.buf_len).min(data.len());
self.buf[self.buf_len..self.buf_len + take].copy_from_slice(&data[..take]);
self.buf_len += take;
data = &data[take..];
if self.buf_len == 64 {
let block = self.buf;
self.compress(&block);
self.buf_len = 0;
}
}
while data.len() >= 64 {
let mut block = [0u8; 64];
block.copy_from_slice(&data[..64]);
self.compress(&block);
data = &data[64..];
}
if !data.is_empty() {
self.buf[..data.len()].copy_from_slice(data);
self.buf_len = data.len();
}
}
fn compress(&mut self, block: &[u8; 64]) {
let mut w = [0u32; 64];
for (i, chunk) in block.chunks_exact(4).enumerate() {
w[i] = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
}
for i in 16..64 {
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16]
.wrapping_add(s0)
.wrapping_add(w[i - 7])
.wrapping_add(s1);
}
let mut h = self.state;
for i in 0..64 {
let s1 = h[4].rotate_right(6) ^ h[4].rotate_right(11) ^ h[4].rotate_right(25);
let ch = (h[4] & h[5]) ^ ((!h[4]) & h[6]);
let t1 = h[7]
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(Self::K[i])
.wrapping_add(w[i]);
let s0 = h[0].rotate_right(2) ^ h[0].rotate_right(13) ^ h[0].rotate_right(22);
let maj = (h[0] & h[1]) ^ (h[0] & h[2]) ^ (h[1] & h[2]);
let t2 = s0.wrapping_add(maj);
h[7] = h[6];
h[6] = h[5];
h[5] = h[4];
h[4] = h[3].wrapping_add(t1);
h[3] = h[2];
h[2] = h[1];
h[1] = h[0];
h[0] = t1.wrapping_add(t2);
}
for i in 0..8 {
self.state[i] = self.state[i].wrapping_add(h[i]);
}
}
fn hex(mut self) -> String {
let bit_len = self.len.wrapping_mul(8);
self.update(&[0x80]);
while self.buf_len != 56 {
self.update(&[0x00]);
}
self.update(&bit_len.to_be_bytes());
let mut out = String::with_capacity(64);
for word in self.state {
out.push_str(&format!("{word:08x}"));
}
out
}
}