use std::fs;
use std::io;
use std::path::Path;
use std::process::{Command, Stdio};
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=frontend/index.html");
println!("cargo:rerun-if-changed=frontend/package.json");
println!("cargo:rerun-if-changed=frontend/package-lock.json");
println!("cargo:rerun-if-changed=frontend/svelte.config.js");
println!("cargo:rerun-if-changed=frontend/tsconfig.json");
println!("cargo:rerun-if-changed=frontend/vite.config.ts");
println!("cargo:rerun-if-changed=frontend/public");
println!("cargo:rerun-if-changed=frontend/src");
let frontend_dir = Path::new("frontend");
let dist_index = frontend_dir.join("dist").join("index.html");
let frontend_src = frontend_dir.join("src");
let prebuilt = dist_index.is_file() && !frontend_src.is_dir();
let backend_build_id = hash_paths(&["build.rs", "Cargo.toml", "Cargo.lock", "src"])
.unwrap_or_else(|error| panic!("failed to hash backend build inputs: {}", error));
let frontend_build_id = if prebuilt {
"prebuilt".to_string()
} else {
hash_paths(&[
"frontend/index.html",
"frontend/package.json",
"frontend/package-lock.json",
"frontend/svelte.config.js",
"frontend/tsconfig.json",
"frontend/vite.config.ts",
"frontend/public",
"frontend/src",
])
.unwrap_or_else(|error| panic!("failed to hash frontend build inputs: {}", error))
};
println!("cargo:rustc-env=WSB_BACKEND_BUILD_ID={}", backend_build_id);
println!("cargo:rustc-env=WSB_FRONTEND_BUILD_ID={}", frontend_build_id);
if prebuilt {
return;
}
let package_json = frontend_dir.join("package.json");
let package_lock = frontend_dir.join("package-lock.json");
let node_modules_dir = frontend_dir.join("node_modules");
let install_stamp = node_modules_dir.join(".wsb-package-lock");
if !frontend_dir.is_dir() {
panic!("frontend/ directory not found");
}
if !package_json.is_file() {
panic!("frontend/package.json not found");
}
let install_subcommand = if package_lock.is_file() {
"ci"
} else {
panic!("frontend/package-lock.json not found; locked frontend dependencies are required");
};
let package_lock_contents = fs::read(&package_lock).unwrap_or_else(|error| {
panic!(
"failed to read {}: {}",
package_lock.display(),
error
)
});
let install_stamp_contents = fs::read(&install_stamp).ok();
let dependencies_ready = node_modules_dir.is_dir()
&& install_stamp_contents
.as_ref()
.is_some_and(|bytes| bytes.as_slice() == package_lock_contents.as_slice());
if !dependencies_ready {
run_npm(
frontend_dir,
&[install_subcommand],
"frontend dependency install",
&frontend_build_id,
);
fs::write(&install_stamp, &package_lock_contents).unwrap_or_else(|error| {
panic!(
"failed to write frontend dependency stamp {}: {}",
install_stamp.display(),
error
)
});
}
run_npm(
frontend_dir,
&["run", "build"],
"frontend production build",
&frontend_build_id,
);
if !dist_index.is_file() {
panic!("frontend build completed without producing frontend/dist/index.html");
}
}
fn run_npm(frontend_dir: &Path, args: &[&str], phase: &str, build_id: &str) {
let mut command = Command::new("npm");
command
.args(args)
.current_dir(frontend_dir)
.env("WSB_FRONTEND_BUILD_ID", build_id)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let status = command.status().unwrap_or_else(|error| {
panic!(
"failed to start npm for {} in {}: {}",
phase,
frontend_dir.display(),
error
)
});
if !status.success() {
panic!(
"npm {} failed during {} in {}",
args.join(" "),
phase,
frontend_dir.display()
);
}
}
fn hash_paths(paths: &[&str]) -> io::Result<String> {
let mut hasher = Fnv1a64::new();
for path in paths {
hash_path(Path::new(path), &mut hasher)?;
}
Ok(hasher.finish_hex())
}
fn hash_path(path: &Path, hasher: &mut Fnv1a64) -> io::Result<()> {
let metadata = fs::metadata(path)?;
if metadata.is_dir() {
let mut entries = fs::read_dir(path)?.collect::<Result<Vec<_>, _>>()?;
entries.sort_by(|left, right| left.path().cmp(&right.path()));
hasher.write(path.to_string_lossy().as_bytes());
hasher.write(b"\0dir\0");
for entry in entries {
hash_path(&entry.path(), hasher)?;
}
return Ok(());
}
hasher.write(path.to_string_lossy().as_bytes());
hasher.write(b"\0file\0");
hasher.write(&fs::read(path)?);
hasher.write(b"\0");
Ok(())
}
struct Fnv1a64(u64);
impl Fnv1a64 {
const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const PRIME: u64 = 0x00000100000001B3;
fn new() -> Self {
Self(Self::OFFSET_BASIS)
}
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.0 ^= u64::from(*byte);
self.0 = self.0.wrapping_mul(Self::PRIME);
}
}
fn finish_hex(&self) -> String {
format!("{:016x}", self.0)
}
}