use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::SystemTime;
const STYLESHEET_SOURCE: &str = "ui/app.css";
const STYLESHEET: &str = "ui/dist/app.css";
const HTMX: &str = "ui/vendor/htmx-2.0.11.min.js";
const SCRIPT: &str = "ui/main.js";
const TEMPLATES: &str = "templates";
fn main() {
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap());
for path in [STYLESHEET_SOURCE, STYLESHEET, HTMX, SCRIPT, TEMPLATES] {
println!("cargo:rerun-if-changed={path}");
}
println!("cargo:rerun-if-env-changed=TAILWINDCSS");
build_stylesheet(&manifest, &out);
let stylesheet = read(&manifest.join(STYLESHEET));
let mut script = read(&manifest.join(HTMX));
script.extend_from_slice(b"\n;\n");
script.extend_from_slice(&read(&manifest.join(SCRIPT)));
let script_path = out.join("app.js");
write(&script_path, &script);
let css_name = hashed_name("app", "css", &stylesheet);
let js_name = hashed_name("app", "js", &script);
let generated = format!(
"pub const STYLESHEET: &str = \"/assets/{css_name}\";\n\
pub const SCRIPT: &str = \"/assets/{js_name}\";\n\
pub static UI_ASSETS: &[(&str, &[u8])] = &[\n \
({css_name:?}, include_bytes!({:?})),\n \
({js_name:?}, include_bytes!({:?})),\n];\n",
manifest.join(STYLESHEET).display().to_string(),
script_path.display().to_string(),
);
write(&out.join("ui_assets.rs"), generated.as_bytes());
}
fn build_stylesheet(manifest: &Path, out: &Path) {
let built = manifest.join(STYLESHEET);
if manifest.join("Cargo.toml.orig").exists() {
return;
}
let Some(tailwind) = find_tailwind() else {
if !built.exists() {
panic!(
"{STYLESHEET} is missing and the Tailwind CLI was not found; install it \
(e.g. `brew install tailwindcss`) or set TAILWINDCSS to its path"
);
}
let sources = newest([manifest.join(STYLESHEET_SOURCE), manifest.join(TEMPLATES)]);
if sources > modified(&built) {
println!(
"cargo:warning=the Tailwind CLI was not found, so {STYLESHEET} was not rebuilt \
and may lack classes the templates use; install it (e.g. `brew install \
tailwindcss`) or set TAILWINDCSS"
);
}
return;
};
let fresh = out.join("app.css");
let output = Command::new(&tailwind)
.current_dir(manifest)
.args(["--input", STYLESHEET_SOURCE, "--output"])
.arg(&fresh)
.arg("--minify")
.output()
.unwrap_or_else(|e| panic!("running {}: {e}", tailwind.display()));
if !output.status.success() {
panic!(
"{} failed:\n{}",
tailwind.display(),
String::from_utf8_lossy(&output.stderr)
);
}
let fresh = read(&fresh);
if std::fs::read(&built).ok().as_deref() != Some(fresh.as_slice()) {
if let Some(dir) = built.parent() {
std::fs::create_dir_all(dir).unwrap();
}
write(&built, &fresh);
}
}
fn find_tailwind() -> Option<PathBuf> {
if let Some(path) = std::env::var_os("TAILWINDCSS").filter(|p| !p.is_empty()) {
return Some(PathBuf::from(path));
}
let path = std::env::var_os("PATH").unwrap_or_default();
std::env::split_paths(&path)
.chain(["/opt/homebrew/bin", "/usr/local/bin"].map(PathBuf::from))
.map(|dir| dir.join("tailwindcss"))
.find(|candidate| candidate.is_file())
}
fn hashed_name(stem: &str, extension: &str, bytes: &[u8]) -> String {
let hash = format!("{:x}", md5::compute(bytes));
format!("{stem}-{}.{extension}", &hash[..10])
}
fn modified(path: &Path) -> SystemTime {
std::fs::metadata(path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH)
}
fn newest(paths: impl IntoIterator<Item = PathBuf>) -> SystemTime {
paths
.into_iter()
.flat_map(|path| {
let mut times = vec![modified(&path)];
if let Ok(entries) = std::fs::read_dir(&path) {
times.extend(entries.flatten().map(|e| modified(&e.path())));
}
times
})
.max()
.unwrap_or(SystemTime::UNIX_EPOCH)
}
fn read(path: &Path) -> Vec<u8> {
std::fs::read(path).unwrap_or_else(|e| panic!("reading {}: {e}", path.display()))
}
fn write(path: &Path, bytes: &[u8]) {
std::fs::write(path, bytes).unwrap_or_else(|e| panic!("writing {}: {e}", path.display()));
}