use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::process::Command;
use sha2::{Digest, Sha512};
macro_rules! log {
($($arg:tt)*) => {
println!("cargo:warning=[ui] {}", format!($($arg)*))
};
}
const UI_CONFIG_FILES: &[&str] = &[
"package.json",
"package-lock.json",
"index.html",
"vite.config.ts",
"tsconfig.json",
"tsconfig.app.json",
"tsconfig.node.json",
];
fn main() {
let ui_dir = Path::new("ui");
let has_npm_project =
ui_dir.join("package.json").exists() && ui_dir.join("package-lock.json").exists();
let watched = emit_rerun_directives(ui_dir);
log!("watching {watched} UI source files for changes");
let dist = ui_dir.join("dist");
std::fs::create_dir_all(&dist).expect("create ui/dist directory");
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "unknown".to_string());
if !has_npm_project {
let hash = compute_dist_hash(&dist);
log_ui_warning(&[
"Using PRE-COMPILED and PRE-BUILT UI.",
&format!("cory package version: {}", version),
"",
"If you are concerned with supply chain attacks, build UI from source with npm.",
"",
"Verify the UI integrity with the official release:",
&format!(
"https://github.com/panon-btc/cory/releases/tag/v{}",
version
),
"",
&format!("Pre-built UI SHA-512 for cory v{}: {}", version, hash),
]);
return;
}
let hash_marker =
Path::new(&std::env::var("OUT_DIR").unwrap_or_default()).join("ui-build-hash");
let current_hash = hash_ui_sources(ui_dir);
let mut needs_build = true;
if dist.join("index.html").exists() {
if let Ok(cached) = std::fs::read_to_string(&hash_marker) {
if cached.trim() == current_hash {
log!("UI sources unchanged: skipping npm build");
needs_build = false;
}
}
}
if needs_build {
let npm_version = Command::new("npm").arg("--version").output();
match &npm_version {
Ok(o) if o.status.success() => {
let ver = String::from_utf8_lossy(&o.stdout);
log!("npm found: v{}", ver.trim());
}
_ => {
log!("npm not found — skipping UI build");
log_ui_warning(&[
"The server will compile, but the UI will show:",
" \"Cory was built without UI (no NPM at build time)\"",
"",
"Install Node.js + npm and rebuild to get the UI.",
]);
return;
}
}
log!("running `npm ci`...");
if !run_npm_step(&["ci"], ui_dir) {
return;
}
log!("`npm ci` done");
log!("running `npm run build`...");
if !run_npm_step(&["run", "build"], ui_dir) {
return;
}
log!("`npm run build` done, UI assets ready in ui/dist/");
let _ = std::fs::write(&hash_marker, ¤t_hash);
}
let hash = compute_dist_hash(&dist);
log!("Pre-built UI SHA-512 for cory v{}: {}", version, hash);
}
fn run_npm_step(args: &[&str], ui_dir: &Path) -> bool {
let label = format!("npm {}", args.join(" "));
match Command::new("npm").args(args).current_dir(ui_dir).status() {
Ok(s) if s.success() => true,
Ok(s) => {
log_ui_warning(&[
&format!("`{label}` failed (exit code: {s})"),
"",
"UI will not be embedded. Check the build output above.",
]);
false
}
Err(e) => {
log_ui_warning(&[&format!("`{label}` could not be executed: {e}")]);
false
}
}
}
fn log_ui_warning(lines: &[&str]) {
log!("----------------------------------------------------------");
for line in lines {
if line.is_empty() {
log!("");
} else {
log!(" > {}", line);
}
}
log!("----------------------------------------------------------");
}
fn emit_rerun_directives(ui_dir: &Path) -> usize {
let mut count = 0;
for name in UI_CONFIG_FILES {
println!("cargo:rerun-if-changed=ui/{name}");
count += 1;
}
let src = ui_dir.join("src");
if src.exists() {
count += walk_rerun(&src);
}
let public = ui_dir.join("public");
if public.exists() {
count += walk_rerun(&public);
}
count
}
fn hash_ui_sources(ui_dir: &Path) -> String {
let mut hasher = DefaultHasher::new();
for name in UI_CONFIG_FILES {
let path = ui_dir.join(name);
if let Ok(meta) = std::fs::metadata(&path) {
path.display().to_string().hash(&mut hasher);
if let Ok(modified) = meta.modified() {
modified.hash(&mut hasher);
}
meta.len().hash(&mut hasher);
}
}
for dir_name in &["src", "public"] {
let dir = ui_dir.join(dir_name);
if dir.exists() {
for path in collect_files(&dir) {
if let Ok(meta) = std::fs::metadata(&path) {
path.display().to_string().hash(&mut hasher);
if let Ok(modified) = meta.modified() {
modified.hash(&mut hasher);
}
meta.len().hash(&mut hasher);
}
}
}
}
format!("{:016x}", hasher.finish())
}
fn collect_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
collect_recursive(dir, &mut files);
files.sort();
files
}
fn collect_recursive(dir: &Path, files: &mut Vec<PathBuf>) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_recursive(&path, files);
} else {
files.push(path);
}
}
}
}
fn compute_dist_hash(dist_dir: &Path) -> String {
let mut hasher = Sha512::new();
for path in collect_files(dist_dir) {
if let Ok(relative) = path.strip_prefix(dist_dir) {
hasher.update(relative.to_string_lossy().as_bytes());
}
if let Ok(content) = std::fs::read(&path) {
hasher.update(&content);
}
}
hex::encode(hasher.finalize())
}
fn walk_rerun(dir: &Path) -> usize {
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
let mut count = 0;
println!("cargo:rerun-if-changed={}", dir.display());
count += 1;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
count += walk_rerun(&path);
} else {
println!("cargo:rerun-if-changed={}", path.display());
count += 1;
}
}
count
}