use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let crate_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest directory"));
let source_web_dir = crate_dir.join("web");
let output = PathBuf::from(env::var_os("OUT_DIR").expect("build output directory"));
let web_dir = output.join("web-build");
let dist_dir = web_dir.join("dist");
for path in [
"package.json",
"package-lock.json",
"index.html",
"vite.config.ts",
"tsconfig.json",
"tsconfig.app.json",
"tsconfig.node.json",
"src",
"public",
] {
println!("cargo:rerun-if-changed={}", source_web_dir.join(path).display());
copy_path(&source_web_dir.join(path), &web_dir.join(path));
}
if !web_dir.join("node_modules").is_dir() {
run(&web_dir, "npm", &["ci"]);
}
run(&web_dir, "npm", &["run", "build"]);
let mut files = Vec::new();
collect_files(&dist_dir, &dist_dir, &mut files);
files.sort_by(|left, right| left.0.cmp(&right.0));
assert!(
files.iter().any(|(path, _)| path == "index.html"),
"frontend build did not produce index.html"
);
let mut generated = String::from(
"pub fn embedded_asset(path: &str) -> Option<EmbeddedAsset> {\n match path {\n",
);
for (relative, absolute) in files {
generated.push_str(&format!(
" {:?} => Some(EmbeddedAsset {{ bytes: &include_bytes!({:?})[..], content_type: {:?} }}),\n",
relative,
absolute,
content_type(&relative),
));
}
generated.push_str(" _ => None,\n }\n}\n");
fs::write(output.join("embedded_web.rs"), generated).expect("write embedded asset table");
}
fn copy_path(source: &Path, destination: &Path) {
if source.is_dir() {
fs::create_dir_all(destination).expect("create frontend build directory");
for entry in fs::read_dir(source).expect("read frontend source directory") {
let entry = entry.expect("read frontend source entry");
copy_path(&entry.path(), &destination.join(entry.file_name()));
}
} else {
fs::create_dir_all(destination.parent().expect("frontend destination parent"))
.expect("create frontend destination parent");
fs::copy(source, destination).expect("copy frontend build input");
}
}
fn run(cwd: &Path, program: &str, args: &[&str]) {
let status = Command::new(program)
.args(args)
.current_dir(cwd)
.status()
.unwrap_or_else(|error| panic!("failed to run {program}: {error}"));
assert!(status.success(), "{program} {} failed", args.join(" "));
}
fn collect_files(root: &Path, directory: &Path, output: &mut Vec<(String, String)>) {
for entry in fs::read_dir(directory).expect("read frontend build directory") {
let entry = entry.expect("read frontend build entry");
let path = entry.path();
if path.is_dir() {
collect_files(root, &path, output);
} else {
let relative = path
.strip_prefix(root)
.expect("asset below build directory")
.to_string_lossy()
.replace('\\', "/");
output.push((
relative,
path.canonicalize()
.expect("canonical asset path")
.to_string_lossy()
.into_owned(),
));
}
}
}
fn content_type(path: &str) -> &'static str {
match Path::new(path)
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or("")
{
"html" => "text/html; charset=utf-8",
"js" => "text/javascript; charset=utf-8",
"css" => "text/css; charset=utf-8",
"json" => "application/json",
"svg" => "image/svg+xml",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"webp" => "image/webp",
"ico" => "image/x-icon",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
_ => "application/octet-stream",
}
}