use std::env; use std::fs; use std::io::Write; use std::path::Path; use std::process::Command; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); build_webui(Path::new(&out_dir)); println!("cargo:rerun-if-changed=resources/skills"); let skills_dir = Path::new("resources/skills"); let skills_out_dir = Path::new(&out_dir).join("skills"); fs::create_dir_all(&skills_out_dir).unwrap(); let mut skills = Vec::new(); if let Ok(entries) = fs::read_dir(skills_dir) { for entry in entries.flatten() { let path = entry.path(); if !path.is_dir() { continue; } let skill_name = path.file_name().unwrap().to_str().unwrap().to_string(); let compressed_path = skills_out_dir.join(format!("{}.tar.zst", skill_name)); let compressed = compress_skill_dir(&path); fs::write(&compressed_path, &compressed).unwrap(); skills.push(skill_name); } } let mut code = String::from( r#"pub struct EmbeddedSkill { pub name: &'static str, pub data: &'static [u8], } pub static EMBEDDED_SKILLS: &[EmbeddedSkill] = &[ "#, ); for name in &skills { let file_path = skills_out_dir .join(format!("{}.tar.zst", name)) .to_string_lossy() .to_string(); code.push_str(&format!( " EmbeddedSkill {{ name: \"{}\", data: include_bytes!(\"{}\") }},\n", name, file_path )); } code.push_str("];\n"); let generated_path = Path::new(&out_dir).join("embedded_skills.rs"); let mut f = fs::File::create(&generated_path).unwrap(); f.write_all(code.as_bytes()).unwrap(); } fn build_webui(out_dir: &Path) { for path in [ "webui/src", "webui/index.html", "webui/jsconfig.json", "webui/package.json", "webui/package-lock.json", "webui/svelte.config.js", "webui/vite.config.js", ] { println!("cargo:rerun-if-changed={path}"); } let webui_dir = Path::new("webui"); let lockfile = webui_dir.join("package-lock.json"); let dependency_stamp = webui_dir .join("node_modules") .join(".picobot-package-lock.json"); let lockfile_contents = fs::read(&lockfile).expect("failed to read webui/package-lock.json"); let dependencies_current = fs::read(&dependency_stamp) .is_ok_and(|stamp| stamp == lockfile_contents) && webui_dir.join("node_modules/.bin/vite").is_file(); if !dependencies_current { run_npm(webui_dir, &["ci", "--no-audit", "--no-fund"], None); fs::write(&dependency_stamp, &lockfile_contents) .expect("failed to write WebUI dependency stamp"); } let webui_out_dir = out_dir.join("webui"); run_npm(webui_dir, &["run", "build"], Some(&webui_out_dir)); } fn run_npm(webui_dir: &Path, args: &[&str], output_dir: Option<&Path>) { let mut command = Command::new("npm"); command.args(args).current_dir(webui_dir); if let Some(output_dir) = output_dir { command.env("PICOBOT_WEBUI_OUT_DIR", output_dir); } let output = command.output().unwrap_or_else(|error| { panic!("failed to start npm for the WebUI build ({error}); install Node.js 20+ and npm") }); if !output.status.success() { panic!( "WebUI command `npm {}` failed\nstdout:\n{}\nstderr:\n{}", args.join(" "), String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } } fn compress_skill_dir(dir: &Path) -> Vec { let mut buf = Vec::new(); let mut builder = tar::Builder::new(&mut buf); builder.follow_symlinks(false); builder .append_dir_all(dir.file_name().unwrap().to_str().unwrap(), dir) .unwrap(); drop(builder); zstd::encode_all(buf.as_slice(), 3).unwrap() }