//! Resolve the SPA bundle embedded by `web.rs` via `include_dir!`. //! //! Three modes, in order: //! //! * `ARR_WEB_DIST` set — a prebuilt bundle (Nix, CI, release pipeline) is //! embedded as-is. Missing or empty means a broken pipeline: fail. //! * pnpm available — the default developer path: build `web/` and embed //! `web/dist`. Failures in the build itself fail loudly. //! * pnpm missing, debug profile — embed a generated placeholder page and //! warn. This keeps `cargo clippy`/`cargo test` working on runners with no //! node toolchain (the CI rust job, DESIGN.md §12). //! //! A release build never gets the placeholder: shipping a binary whose UI is //! an apology defeats the single-binary acceptance test, so that combination //! panics instead. use std::env; use std::path::PathBuf; use std::process::Command; fn main() { println!("cargo:rerun-if-env-changed=ARR_WEB_DIST"); let dist = match env::var_os("ARR_WEB_DIST") { Some(prebuilt) => PathBuf::from(prebuilt), None => build_or_placeholder(), }; let index = dist.join("index.html"); let bundled = std::fs::metadata(&index).is_ok_and(|m| m.is_file() && m.len() > 0); assert!( bundled, "no SPA bundle at {} (missing or empty index.html) — check the vite \ outDir or the ARR_WEB_DIST override", index.display() ); // `include_dir!` expands `$VAR` against the env rustc is invoked with, // which is what `cargo:rustc-env` sets. println!("cargo:rustc-env=ARR_WEB_BUNDLE_DIR={}", dist.display()); } fn build_or_placeholder() -> PathBuf { let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("cargo sets CARGO_MANIFEST_DIR")); let web_dir = manifest_dir.join("../../web"); // Rebuild only when a frontend input changes, not on unrelated Rust // recompiles. The dist watch matters too: the inputs decide whether this // script reruns, but `include_dir!` reads dist, and without this watch an // incremental build can regenerate dist yet keep the old bytes embedded. for rel in [ "src", "index.html", "package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml", "vite.config.ts", "tsconfig.json", "dist", ] { println!("cargo:rerun-if-changed={}", web_dir.join(rel).display()); } if !tool_works("pnpm") { let profile = env::var("PROFILE").unwrap_or_default(); assert!( profile != "release", "arr embeds the web SPA at build time and needs pnpm + node for a \ release build. Install them, or point ARR_WEB_DIST at a prebuilt \ bundle." ); println!( "cargo:warning=pnpm not found; embedding a placeholder page instead of \ the web SPA (debug builds only)" ); return write_placeholder(); } run( Command::new("pnpm") .args(["install", "--frozen-lockfile"]) .current_dir(&web_dir), "pnpm install --frozen-lockfile", ); run( Command::new("pnpm").arg("build").current_dir(&web_dir), "pnpm build", ); web_dir.join("dist") } fn write_placeholder() -> PathBuf { let out = PathBuf::from(env::var("OUT_DIR").expect("cargo sets OUT_DIR")).join("placeholder"); std::fs::create_dir_all(&out).expect("create placeholder dir"); std::fs::write(out.join("index.html"), PLACEHOLDER_HTML).expect("write placeholder"); out } fn tool_works(tool: &str) -> bool { Command::new(tool) .arg("--version") .output() .is_ok_and(|o| o.status.success()) } fn run(cmd: &mut Command, label: &str) { let status = cmd .status() .unwrap_or_else(|e| panic!("failed to spawn `{label}`: {e}")); assert!(status.success(), "`{label}` failed with {status}"); } const PLACEHOLDER_HTML: &str = r#"
but this binary was built without the web bundle (no pnpm on the build
machine). Rebuild with pnpm + node installed, or set ARR_WEB_DIST
to a prebuilt bundle.