use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let web_src = manifest_dir.join("web");
let web_out = out_dir.join("web");
let web_dist_out = web_out.join("dist");
for f in [
"web/src",
"web/package.json",
"web/package-lock.json",
"web/index.html",
"web/vite.config.ts",
"web/tsconfig.json",
] {
println!("cargo:rerun-if-changed={f}");
}
println!("cargo:rerun-if-env-changed=SKIP_WEB_BUILD");
if std::env::var_os("SKIP_WEB_BUILD").is_some() {
if !web_dist_out.join("index.html").exists() {
copy_dist(&web_src.join("dist"), &web_dist_out);
}
return;
}
let npm = which("npm");
if npm.is_none() {
println!("cargo:warning=npm not found; using prebuilt web/dist (set SKIP_WEB_BUILD=1 to silence)");
if !web_dist_out.join("index.html").exists() {
copy_dist(&web_src.join("dist"), &web_dist_out);
}
return;
}
let npm = npm.unwrap();
sync_web_src(&web_src, &web_out);
if !web_out.join("node_modules").exists() {
run(&npm, &["ci", "--silent"], &web_out, &[]);
}
run(
&npm,
&["run", "build", "--silent"],
&web_out,
&[("VITE_OUT_DIR", web_dist_out.to_str().unwrap())],
);
ensure_dist_exists(&web_dist_out);
}
fn sync_web_src(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap_or_else(|e| panic!("create {dst:?}: {e}"));
for entry in std::fs::read_dir(src).unwrap_or_else(|e| panic!("readdir {src:?}: {e}")) {
let entry = entry.unwrap();
let name = entry.file_name();
if matches!(
name.to_str(),
Some("node_modules") | Some("dist") | Some(".vite")
) {
continue;
}
let dst_path = dst.join(&name);
if entry.file_type().unwrap().is_dir() {
copy_dir_all(&entry.path(), &dst_path)
.unwrap_or_else(|e| panic!("copy dir {:?}: {e}", entry.path()));
} else {
std::fs::copy(entry.path(), &dst_path)
.unwrap_or_else(|e| panic!("copy {:?}: {e}", entry.path()));
}
}
}
fn copy_dist(src: &Path, dst: &Path) {
if !src.join("index.html").exists() {
panic!(
"web/dist/index.html missing at {}. Run `npm --prefix web run build` first.",
src.display()
);
}
copy_dir_all(src, dst).unwrap_or_else(|e| panic!("copy {src:?} -> {dst:?}: {e}"));
}
fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let dst_path = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_dir_all(&entry.path(), &dst_path)?;
} else {
std::fs::copy(entry.path(), dst_path)?;
}
}
Ok(())
}
fn ensure_dist_exists(dist: &Path) {
let idx = dist.join("index.html");
if !idx.exists() {
panic!("web/dist/index.html missing at {}", idx.display());
}
}
fn which(cmd: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let full = dir.join(cmd);
if full.is_file() {
return Some(full);
}
}
None
}
fn run(cmd: &Path, args: &[&str], cwd: &Path, env: &[(&str, &str)]) {
let mut command = Command::new(cmd);
command.args(args).current_dir(cwd);
for (k, v) in env {
command.env(k, v);
}
let status = command
.status()
.unwrap_or_else(|e| panic!("run {} {:?}: {e}", cmd.display(), args));
if !status.success() {
panic!("{} {:?} failed: {status}", cmd.display(), args);
}
}