use std::collections::BTreeMap; use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; fn main() { println!("cargo:rerun-if-changed=build.rs"); for watched in [ "src", "index.html", "vite.config.ts", "package.json", "bun.lock", "tsconfig.json", "tsconfig.app.json", "tsconfig.node.json", ] { println!("cargo:rerun-if-changed={watched}"); } let root = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR")); let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")); if env::var_os("ADMIN_UI_SKIP_FRONTEND_BUILD").is_none() && !build_frontend(&root) { println!( "cargo:warning=admin frontend build via bun/npm unavailable or failed; \ embedding existing dist/ as-is" ); } let dist = root.join("dist"); let index = dist.join("index.html"); if !index.is_file() { panic!( "dist/index.html not found — run `bun install && bun run build` \ (or `npm run build`) inside admin/ first" ); } let mut files: BTreeMap = BTreeMap::new(); collect(&dist, &dist, &mut files); let mut src = String::from("// @generated by build.rs — embedded admin frontend (dist/)\n\n"); src.push_str(&format!("pub const INDEX: &[u8] = include_bytes!({:?});\n\n", index)); src.push_str("pub const FILES: &[(&str, &[u8])] = &[\n"); for (rel, abs) in &files { if rel == "index.html" { continue; } src.push_str(&format!(" ({:?}, include_bytes!({:?})),\n", rel, abs)); } src.push_str("];\n\npub const PATHS: &[&str] = &[\n"); for rel in files.keys() { src.push_str(&format!(" {:?},\n", rel)); } src.push_str("];\n"); fs::write(out_dir.join("dist_embed.rs"), src).expect("write dist_embed.rs"); } fn build_frontend(root: &Path) -> bool { if !root.join("package.json").is_file() { return false; } let tool = if command_ok("bun") { "bun" } else if command_ok("npm") { npm_cmd() } else { return false; }; let mut steps: Vec<(&str, &[&str])> = Vec::new(); if !root.join("node_modules").is_dir() { steps.push((tool, &["install"])); } steps.push((tool, &["run", "build"])); for (t, args) in steps { eprintln!("cargo:warning=running `{t} {}` in {}", args.join(" "), root.display()); let ok = Command::new(t) .args(args) .current_dir(root) .status() .map(|s| s.success()) .unwrap_or(false); if !ok { return false; } } true } #[cfg(windows)] fn npm_cmd() -> &'static str { "npm.cmd" } #[cfg(not(windows))] fn npm_cmd() -> &'static str { "npm" } fn command_ok(name: &str) -> bool { Command::new(name) .arg("--version") .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() .is_ok_and(|s| s.success()) } fn collect(base: &Path, dir: &Path, out: &mut BTreeMap) { for entry in fs::read_dir(dir).expect("read dist dir") { let path = entry.expect("dist entry").path(); if path.is_dir() { collect(base, &path, out); } else { let rel = path .strip_prefix(base) .unwrap() .to_string_lossy() .replace('\\', "/"); out.insert(rel, path); } } }