use std::env;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
fn main() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let webui_dir = Path::new(&manifest_dir).join("webui");
println!("cargo:rerun-if-changed={}", webui_dir.display());
println!("cargo:rerun-if-changed=src");
let sha = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.current_dir(&manifest_dir)
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "desconhecido".to_string());
println!("cargo:rustc-env=RUSTPLOY_GIT_SHA={sha}");
let mut rel_paths = Vec::new();
collect_files(&webui_dir, &webui_dir, &mut rel_paths);
rel_paths.sort();
let agg_hash = {
let mut h = FNV_OFFSET;
for rel in &rel_paths {
h = fnv1a(h, fs::read(webui_dir.join(rel)).unwrap().as_slice());
}
format!("{:016x}", h)[..8].to_string()
};
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let assets_bin_dir = out_dir.join("webui");
fs::create_dir_all(&assets_bin_dir).unwrap();
let mut code =
String::from("// Gerado por build.rs a partir de webui/ — não editar à mão.\n&[\n");
for (i, rel) in rel_paths.iter().enumerate() {
let raw = fs::read(webui_dir.join(rel)).unwrap();
let route = route_for(rel);
let content_type = content_type_for(rel);
let is_html = rel.ends_with(".html");
let is_js = rel.ends_with(".js");
let is_text_asset =
is_html || is_js || rel.ends_with(".css") || rel.ends_with(".webmanifest");
let bytes = if is_text_asset {
let text = String::from_utf8(raw)
.unwrap_or_else(|_| panic!("webui/{rel}: esperado UTF-8 (é um asset de texto)"));
let processed = if rel == "sw.js" {
text.replace("__CACHE_VERSION__", &agg_hash)
} else {
text
};
minify(&processed, is_html).into_bytes()
} else {
raw
};
let gz = gzip(&bytes);
let bin_name = format!("{i}.gz");
fs::write(assets_bin_dir.join(&bin_name), &gz).unwrap();
let etag = format!("{:016x}", fnv1a(FNV_OFFSET, &bytes));
let no_cache = is_html || rel == "sw.js";
code.push_str(&format!(
" Asset {{ route: {route:?}, content_type: {content_type:?}, etag: {etag:?}, no_cache: {no_cache}, gz: include_bytes!(concat!(env!(\"OUT_DIR\"), \"/webui/{bin_name}\")) }},\n"
));
}
code.push_str("]\n");
fs::write(out_dir.join("webui_assets.rs"), code).unwrap();
}
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) {
for entry in fs::read_dir(dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
collect_files(root, &path, out);
} else {
let rel = path
.strip_prefix(root)
.unwrap()
.to_str()
.unwrap()
.replace('\\', "/");
out.push(rel);
}
}
}
fn route_for(rel: &str) -> String {
if rel == "index.html" {
"/".to_string()
} else {
format!("/{rel}")
}
}
fn content_type_for(rel: &str) -> &'static str {
if rel.ends_with(".html") {
"text/html; charset=utf-8"
} else if rel.ends_with(".css") {
"text/css; charset=utf-8"
} else if rel.ends_with(".js") {
"text/javascript; charset=utf-8"
} else if rel.ends_with(".webmanifest") {
"application/manifest+json"
} else if rel.ends_with(".png") {
"image/png"
} else if rel.ends_with(".svg") {
"image/svg+xml"
} else {
"application/octet-stream"
}
}
fn minify(src: &str, is_html: bool) -> String {
if !is_html {
return src.to_string();
}
let bytes = src.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i..].starts_with(b"<!--") {
if let Some(end) = src[i..].find("-->") {
i += end + 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8(out).expect("minify: entrada UTF-8 válida deve produzir saída UTF-8 válida")
}
fn gzip(data: &[u8]) -> Vec<u8> {
let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
enc.write_all(data).unwrap();
enc.finish().unwrap()
}
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
fn fnv1a(mut h: u64, data: &[u8]) -> u64 {
for &b in data {
h ^= b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h
}