use std::{
fs,
path::{Path, PathBuf},
};
const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
fn fnv1a_update(mut hash: u64, bytes: &[u8]) -> u64 {
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
fn collect_files(dir: &Path, root: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_files(&path, root, out);
} else if let Ok(rel) = path.strip_prefix(root) {
out.push(rel.to_path_buf());
}
}
}
fn normalize_path(rel: &Path) -> String {
rel.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/")
}
fn generate_transport_shaders(manifest_dir: &Path, out_dir: &Path) {
let shaders_dir = manifest_dir.join("src").join("renderer").join("shaders");
let read = |name: &str| -> String {
fs::read_to_string(shaders_dir.join(name))
.unwrap_or_else(|e| panic!("failed to read shaders/{name}: {e}"))
};
let prelude = read("transport_physics.wgsl");
let bounce_shared = read("transport_bounce.wgsl");
let units: [(&str, bool); 3] = [
("spectral_transport.wgsl", true),
("transport_functions.wgsl", false),
("wavefront_transport.wgsl", true),
];
for (name, needs_bounce_shared) in units {
let body = read(name);
let generated = if needs_bounce_shared {
format!(
"// GENERATED by build.rs: shaders/transport_physics.wgsl + \
shaders/transport_bounce.wgsl + shaders/{name}.\n\
// Do not edit this file -- edit the sources above instead.\n\n\
{prelude}\n{bounce_shared}\n{body}"
)
} else {
format!(
"// GENERATED by build.rs: shaders/transport_physics.wgsl + shaders/{name}.\n\
// Do not edit this file -- edit the two sources above instead.\n\n{prelude}\n{body}"
)
};
let out_name = name.replace(".wgsl", ".generated.wgsl");
fs::write(out_dir.join(&out_name), generated)
.unwrap_or_else(|e| panic!("failed to write {out_name} to OUT_DIR: {e}"));
}
}
fn main() {
let manifest_dir = PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"),
);
let src_dir = manifest_dir.join("src");
let cargo_toml = manifest_dir.join("Cargo.toml");
let out_dir =
PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo for build scripts"));
generate_transport_shaders(&manifest_dir, &out_dir);
let mut rel_paths = Vec::new();
collect_files(&src_dir, &manifest_dir, &mut rel_paths);
rel_paths.retain(|p| {
p.extension()
.is_some_and(|ext| ext == "rs" || ext == "wgsl")
});
if rel_paths.is_empty() {
println!("cargo:rerun-if-changed={}", src_dir.display());
println!("cargo:rustc-env=INDICATRIX_SOURCE_HASH=unknown");
println!("cargo:rustc-env=INDICATRIX_BUILD_ID=unknown");
return;
}
rel_paths.push(PathBuf::from("Cargo.toml"));
rel_paths.sort();
let mut hash = FNV_OFFSET_BASIS;
for rel in &rel_paths {
let abs = manifest_dir.join(rel);
let Ok(contents) = fs::read(&abs) else {
continue;
};
let normalized: Vec<u8> = {
let mut out = Vec::with_capacity(contents.len());
let mut i = 0;
while i < contents.len() {
if contents[i] == b'\r' && contents.get(i + 1) == Some(&b'\n') {
} else {
out.push(contents[i]);
}
i += 1;
}
out
};
let rel_str = normalize_path(rel);
hash = fnv1a_update(hash, rel_str.as_bytes());
hash = fnv1a_update(hash, &[0u8]); hash = fnv1a_update(hash, &normalized);
println!("cargo:rerun-if-changed={}", abs.display());
}
println!("cargo:rerun-if-changed={}", src_dir.display());
println!("cargo:rerun-if-changed={}", cargo_toml.display());
let source_hash = format!("{hash:016x}");
println!("cargo:rustc-env=INDICATRIX_SOURCE_HASH={source_hash}");
let build_id = match std::env::var("CARGO_PKG_VERSION") {
Ok(version) if !version.trim().is_empty() => {
format!(
"{:016x}",
fnv1a_update(FNV_OFFSET_BASIS, version.trim().as_bytes())
)
}
_ => "unknown".to_string(),
};
println!("cargo:rustc-env=INDICATRIX_BUILD_ID={build_id}");
}