use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use hf_hub::api::sync::ApiBuilder;
const SRC_WEIGHTS_DIR: &str = "src/text_search/embed_weights";
const MODEL_ID: &str = "sentence-transformers/all-MiniLM-L6-v2";
const DEFAULT_ENDPOINT: &str = "https://huggingface.co";
struct Asset {
local_name: &'static str,
remote_path: &'static str,
size: u64,
rust_const: &'static str,
}
const ASSETS: &[Asset] = &[
Asset {
local_name: "model.safetensors",
remote_path: "model.safetensors",
size: 90868376,
rust_const: "MODEL_WEIGHTS",
},
Asset {
local_name: "tokenizer.json",
remote_path: "tokenizer.json",
size: 466247,
rust_const: "TOKENIZER_JSON",
},
Asset {
local_name: "config.json",
remote_path: "config.json",
size: 612,
rust_const: "CONFIG_JSON",
},
];
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let src_weights = manifest_dir.join(SRC_WEIGHTS_DIR);
let out_weights = out_dir.join("embed_weights");
let manifest_path = src_weights.join("MODEL.manifest");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", manifest_path.display());
for a in ASSETS {
println!(
"cargo:rerun-if-changed={}",
src_weights.join(a.local_name).display()
);
}
println!("cargo:rerun-if-env-changed=MYCO_EMBED_CACHE");
println!("cargo:rerun-if-env-changed=MYCO_EMBED_OFFLINE");
println!("cargo:rerun-if-env-changed=MYCO_EMBED_ENDPOINT");
println!("cargo:rerun-if-env-changed=HF_HOME");
println!("cargo:rerun-if-env-changed=HF_HUB_CACHE");
println!("cargo:rerun-if-env-changed=HF_ENDPOINT");
println!("cargo:rerun-if-env-changed=HF_TOKEN");
fs::create_dir_all(&out_weights).unwrap_or_else(|e| {
panic!("create {}: {e}", out_weights.display());
});
let shas = read_manifest_shas(&manifest_path);
let offline = env::var_os("MYCO_EMBED_OFFLINE").is_some_and(|v| v != "0");
let mut seed_dirs: Vec<PathBuf> = Vec::new();
if let Some(cache) = env::var_os("MYCO_EMBED_CACHE").map(PathBuf::from) {
seed_dirs.push(cache);
}
if src_weights.is_dir() {
seed_dirs.push(src_weights.clone());
}
let mut hub_repo = None;
let offline_cache = if offline {
Some(hf_hub::Cache::from_env().model(MODEL_ID.to_string()))
} else {
None
};
for asset in ASSETS {
let dest = out_weights.join(asset.local_name);
let want_sha = shas.get(asset.local_name).map(String::as_str);
if asset_ok(&dest, asset.size, want_sha) {
continue;
}
let mut seeded = false;
for cache_root in &seed_dirs {
let candidates = [
cache_root.join(asset.local_name),
cache_root.join(asset.remote_path),
];
if let Some(src) = candidates.into_iter().find(|p| p.is_file())
&& asset_ok(&src, asset.size, want_sha)
{
fs::copy(&src, &dest).unwrap_or_else(|e| {
panic!("copy {} → {}: {e}", src.display(), dest.display());
});
println!(
"cargo:warning=MiniLM asset {} seeded from {}",
asset.local_name,
src.display()
);
seeded = true;
break;
}
}
if seeded {
continue;
}
if let Some(cache_repo) = offline_cache.as_ref() {
if let Some(cached) = cache_repo.get(asset.remote_path)
&& (asset_ok(&cached, asset.size, want_sha)
|| (want_sha.is_none()
&& asset.local_name != "model.safetensors"
&& fs::metadata(&cached).map(|m| m.len()).unwrap_or(0) > 0))
{
fs::copy(&cached, &dest).unwrap_or_else(|e| {
panic!("copy {} → {}: {e}", cached.display(), dest.display());
});
println!(
"cargo:warning=MiniLM asset {} from offline HF cache {}",
asset.local_name,
cached.display()
);
continue;
}
panic!(
"MYCO_EMBED_OFFLINE set but MiniLM asset missing/invalid: {}.\n\
Not found in MYCO_EMBED_CACHE / {src}/ / HF hub cache.\n\
Seed via network once (unset MYCO_EMBED_OFFLINE), or set MYCO_EMBED_CACHE.\n\
See {src}/README.md",
dest.display(),
src = src_weights.display()
);
}
if hub_repo.is_none() {
hub_repo = Some(build_hub_repo());
}
let repo = hub_repo.as_ref().unwrap();
let cached = match repo.get(asset.remote_path) {
Ok(p) => p,
Err(e) => panic!(
"hf-hub failed to fetch {} from {MODEL_ID}: {e}\n\
See {src}/README.md",
asset.remote_path,
src = src_weights.display()
),
};
if !asset_ok(&cached, asset.size, want_sha) {
let len = fs::metadata(&cached).map(|m| m.len()).unwrap_or(0);
if want_sha.is_none() && len > 0 && asset.local_name != "model.safetensors" {
println!(
"cargo:warning=MiniLM {} size {len} (expected {}); accepting",
asset.local_name, asset.size
);
} else {
panic!(
"MiniLM asset {} failed integrity from hub cache {} (size={len}, expected {})",
asset.local_name,
cached.display(),
asset.size
);
}
}
fs::copy(&cached, &dest).unwrap_or_else(|e| {
panic!("copy {} → {}: {e}", cached.display(), dest.display());
});
println!(
"cargo:warning=MiniLM asset {} from hf-hub cache {}",
asset.local_name,
cached.display()
);
}
write_embed_assets_rs(&out_dir, &out_weights);
println!(
"cargo:warning=MiniLM candle embed assets ready under {}",
out_weights.display()
);
}
fn build_hub_repo() -> hf_hub::api::sync::ApiRepo {
let mut builder = ApiBuilder::from_env().with_progress(false);
if let Ok(endpoint) = env::var("MYCO_EMBED_ENDPOINT") {
let endpoint = endpoint.trim().trim_end_matches('/').to_string();
if !endpoint.is_empty() {
builder = builder.with_endpoint(endpoint);
}
} else if env::var_os("HF_ENDPOINT").is_none() {
builder = builder.with_endpoint(DEFAULT_ENDPOINT.to_string());
}
let api = builder.build().unwrap_or_else(|e| {
panic!("hf-hub ApiBuilder failed: {e}");
});
api.model(MODEL_ID.to_string())
}
fn write_embed_assets_rs(out_dir: &Path, weights: &Path) {
let assets_rs = out_dir.join("embed_assets.rs");
let mut body = String::from(
"// @generated by build.rs — MiniLM assets staged under OUT_DIR/embed_weights\n",
);
for asset in ASSETS {
let path = weights.join(asset.local_name);
let path_lit = path
.to_str()
.unwrap_or_else(|| panic!("non-utf8 path {}", path.display()))
.replace('\\', "/");
body.push_str(&format!(
"pub static {}: &[u8] = include_bytes!(r#\"{}\"#);\n",
asset.rust_const, path_lit
));
}
fs::write(&assets_rs, body).unwrap_or_else(|e| panic!("write {}: {e}", assets_rs.display()));
}
fn asset_ok(path: &Path, size: u64, expected_sha: Option<&str>) -> bool {
let Ok(meta) = fs::metadata(path) else {
return false;
};
if meta.len() != size {
return false;
}
if let Some(want) = expected_sha {
match sha256_file(path) {
Ok(got) if got == want => true,
Ok(got) => {
eprintln!(
"cargo:warning=sha256 mismatch for {} (got {got}, want {want})",
path.display()
);
false
}
Err(e) => {
eprintln!("cargo:warning=sha256 failed for {}: {e}", path.display());
false
}
}
} else {
true
}
}
fn read_manifest_shas(path: &Path) -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
let Ok(text) = fs::read_to_string(path) else {
return map;
};
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let mut parts = line.split_whitespace();
let Some(hash) = parts.next() else { continue };
let Some(name) = parts.next() else { continue };
if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
map.insert(
name.trim_start_matches("./").to_string(),
hash.to_ascii_lowercase(),
);
}
}
map
}
fn sha256_file(path: &Path) -> Result<String, String> {
for (bin, args_prefix) in [("shasum", &["-a", "256"][..]), ("sha256sum", &[][..])] {
let mut cmd = Command::new(bin);
for a in args_prefix {
cmd.arg(a);
}
cmd.arg(path);
if let Ok(out) = cmd.output()
&& out.status.success()
{
let s = String::from_utf8_lossy(&out.stdout);
let hash = s
.split_whitespace()
.next()
.unwrap_or("")
.to_ascii_lowercase();
if hash.len() == 64 {
return Ok(hash);
}
}
}
Err("no shasum/sha256sum on PATH".into())
}