use std::{ env, fs, path::{Path, PathBuf}, }; fn content_hash(content: &str) -> String { let value = content.bytes().fold(0xcbf29ce484222325_u64, |hash, byte| { (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3) }); format!("{value:016x}") } fn write_asset(output: &Path, stem: &str, extension: &str, content: &str) -> String { let file_name = format!("{stem}.{}.{extension}", content_hash(content)); fs::write(output.join(&file_name), content) .unwrap_or_else(|error| panic!("write {file_name}: {error}")); file_name } fn cargo_asset_env(name: &str, file_name: &str) { println!("cargo:rustc-env={name}_FILE={file_name}"); println!("cargo:rustc-env={name}_ROUTE=/assets/{file_name}"); } fn main() { let output = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=web"); println!("cargo:rerun-if-changed=docs/api-ui"); println!("cargo:rerun-if-changed=vendor/swagger-ui/swagger-ui-bundle.js"); println!("cargo:rerun-if-changed=vendor/swagger-ui/swagger-ui.css"); if let Ok(entries) = fs::read_dir(&output) { for entry in entries.flatten() { let name = entry.file_name(); let name = name.to_string_lossy(); let generated_asset = (name.starts_with("app.") || name.starts_with("docs.")) && (name.ends_with(".js") || name.ends_with(".css")); if generated_asset { let _ = fs::remove_file(entry.path()); } } } let manifest = fs::read_to_string("web/assets.list").expect("web/assets.list is required"); let mut js = String::from("/* MikroTik Backup System 0.0.1; generated from web/assets.list */\n"); let mut css = String::new(); for entry in manifest .lines() .map(str::trim) .filter(|value| !value.is_empty() && !value.starts_with('#')) { let source = fs::read_to_string(PathBuf::from("web").join(entry)) .unwrap_or_else(|error| panic!("{entry}: {error}")); if entry.ends_with(".js") { js.push_str(&source); js.push_str("\n;\n"); } else if entry.ends_with(".css") { css.push_str(&source); css.push('\n'); } else { panic!("Unsupported bundle input: {entry}"); } } let app_js_file = write_asset(&output, "app", "js", &js); let app_css_file = write_asset(&output, "app", "css", &css); cargo_asset_env("APP_JS", &app_js_file); cargo_asset_env("APP_CSS", &app_css_file); let html = fs::read_to_string("web/index.html") .expect("web/index.html is required") .replace("/assets/app.js", &format!("/assets/{app_js_file}")) .replace("/assets/app.css", &format!("/assets/{app_css_file}")); fs::write(output.join("index.html"), html).expect("write HTML shell"); let mut docs_js = fs::read_to_string("vendor/swagger-ui/swagger-ui-bundle.js") .expect("Swagger UI JavaScript is required") .lines() .filter(|line| !line.starts_with("//# sourceMappingURL=")) .collect::>() .join("\n"); docs_js.push_str("\n;\n"); docs_js.push_str(&fs::read_to_string("docs/api-ui/init.js").expect("docs API initializer")); let mut docs_css = fs::read_to_string("vendor/swagger-ui/swagger-ui.css") .expect("Swagger UI stylesheet is required") .replace("/*# sourceMappingURL=swagger-ui.css.map*/", ""); docs_css.push('\n'); docs_css.push_str(&fs::read_to_string("docs/api-ui/style.css").expect("docs API stylesheet")); let docs_js_file = write_asset(&output, "docs", "js", &docs_js); let docs_css_file = write_asset(&output, "docs", "css", &docs_css); cargo_asset_env("DOCS_JS", &docs_js_file); cargo_asset_env("DOCS_CSS", &docs_css_file); let docs_html = fs::read_to_string("docs/api-ui/index.html") .expect("docs API HTML is required") .replace("/assets/docs.js", &format!("/assets/{docs_js_file}")) .replace("/assets/docs.css", &format!("/assets/{docs_css_file}")); fs::write(output.join("docs.html"), docs_html).expect("write API documentation HTML"); }