use std::io::Write;
use std::path::Path;
use std::process::Command;
use std::time::SystemTime;
fn main() {
let target = std::env::var("TARGET").expect("Cargo always sets TARGET");
println!("cargo:rustc-env=BUILD_TARGET={target}");
println!("cargo:rerun-if-changed=web/src");
println!("cargo:rerun-if-changed=web/package.json");
println!("cargo:rerun-if-changed=web/package-lock.json");
println!("cargo:rerun-if-changed=web/esbuild.config.mjs");
println!("cargo:rerun-if-changed=web/dist/bundle.js");
let web = Path::new("web");
let bundle = web.join("dist/bundle.js");
if !bundle_is_fresh(&bundle, web) {
assert!(
npm_available(),
"web/dist/bundle.js is missing or outdated and npm is not on PATH.\n\
Install Node.js/npm to rebuild the HTML report bundle, or use a \
prebuilt release binary."
);
assert!(
web.join("package.json").exists(),
"web/package.json not found — cannot build the HTML report bundle"
);
let install_cmd = if web.join("package-lock.json").exists() {
"ci"
} else {
"install"
};
assert!(run_npm(web, &[install_cmd]), "`npm {install_cmd}` failed");
assert!(run_npm(web, &["run", "build"]), "`npm run build` failed");
assert!(
bundle.exists(),
"npm run build did not produce web/dist/bundle.js"
);
}
compress_bundle(&bundle);
copy_raw_bundle(&bundle);
}
fn compress_bundle(bundle: &Path) {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR set by cargo");
let dest = Path::new(&out_dir).join("bundle.deflate");
let src = std::fs::read(bundle).expect("read web/dist/bundle.js");
let mut enc = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best());
enc.write_all(&src).expect("deflate write");
let compressed = enc.finish().expect("deflate finish");
std::fs::write(&dest, &compressed).expect("write bundle.deflate");
}
fn copy_raw_bundle(bundle: &Path) {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR set by cargo");
let dest = Path::new(&out_dir).join("bundle.js");
std::fs::copy(bundle, &dest).expect("copy bundle.js to OUT_DIR");
}
fn bundle_is_fresh(bundle: &Path, web: &Path) -> bool {
if !bundle.exists() {
return false;
}
if !web.join("node_modules").exists() {
return true;
}
let bundle_mtime = match mtime(bundle) {
Some(t) => t,
None => return false,
};
let manifests = [
web.join("package.json"),
web.join("package-lock.json"),
web.join("esbuild.config.mjs"),
];
for src in &manifests {
if mtime(src).map(|t| t > bundle_mtime).unwrap_or(false) {
return false;
}
}
let src_dir = web.join("src");
if src_dir.is_dir() && dir_has_newer(&src_dir, bundle_mtime) {
return false;
}
true
}
fn dir_has_newer(dir: &Path, threshold: SystemTime) -> bool {
let Ok(entries) = std::fs::read_dir(dir) else {
return false;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if dir_has_newer(&path, threshold) {
return true;
}
} else if mtime(&path).map(|t| t > threshold).unwrap_or(false) {
return true;
}
}
false
}
fn mtime(path: &Path) -> Option<SystemTime> {
std::fs::metadata(path).ok()?.modified().ok()
}
fn npm_bin() -> &'static str {
if cfg!(windows) { "npm.cmd" } else { "npm" }
}
fn npm_available() -> bool {
Command::new(npm_bin())
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
fn run_npm(dir: &Path, args: &[&str]) -> bool {
match Command::new(npm_bin()).args(args).current_dir(dir).status() {
Ok(s) => s.success(),
Err(_) => false,
}
}