use std::path::{Path, PathBuf};
use std::process::Command;
const SHADER_DIRS: &[&str] = &[
"src/renderer",
"src/grid/shaders",
];
fn main() {
let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os != "linux" {
println!("cargo:rerun-if-env-changed=CARGO_CFG_TARGET_OS");
return;
}
println!("cargo:rerun-if-env-changed=GLSLC");
println!("cargo:rerun-if-env-changed=GLSLANG_VALIDATOR");
println!("cargo:rerun-if-changed=build.rs");
let out_dir =
PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR must be set by cargo"));
let compiler = locate_compiler();
eprintln!("sugarloaf build.rs: GLSL compiler = {:?}", compiler);
let sources = discover_shaders(SHADER_DIRS);
if sources.is_empty() {
println!(
"cargo:warning=sugarloaf build.rs: no GLSL shaders found in {SHADER_DIRS:?}"
);
return;
}
for src in &sources {
println!("cargo:rerun-if-changed={}", src.display());
compile(&compiler, src, &out_dir);
}
}
fn discover_shaders(dirs: &[&str]) -> Vec<PathBuf> {
use std::collections::HashSet;
let mut out: Vec<PathBuf> = Vec::new();
let mut seen_stems: HashSet<String> = HashSet::new();
for dir in dirs {
println!("cargo:rerun-if-changed={dir}");
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let name = match path.file_name().and_then(|s| s.to_str()) {
Some(n) => n.to_owned(),
None => continue,
};
if !(name.ends_with(".vert.glsl") || name.ends_with(".frag.glsl")) {
continue;
}
let stem = name
.strip_suffix(".glsl")
.expect("just checked the suffix")
.to_owned();
if !seen_stems.insert(stem.clone()) {
panic!(
"sugarloaf build.rs: duplicate shader stem `{stem}` across \
scanned dirs — `OUT_DIR` would collide. Rename one of them."
);
}
out.push(path);
}
}
out.sort();
out
}
#[derive(Debug)]
enum Compiler {
Glslc(PathBuf),
Glslang(PathBuf),
}
fn locate_compiler() -> Compiler {
if let Some(p) = env_path("GLSLC") {
return Compiler::Glslc(p);
}
if let Some(p) = env_path("GLSLANG_VALIDATOR") {
return Compiler::Glslang(p);
}
if let Some(p) = which("glslc") {
return Compiler::Glslc(p);
}
if let Some(p) = which("glslangValidator") {
return Compiler::Glslang(p);
}
panic!(
"\nsugarloaf: no GLSL → SPIR-V compiler found.\n\
Install one of:\n \
* Debian: `apt install glslang-tools` (provides glslangValidator)\n \
* or `apt install glslc` (Google's glslc, Bookworm+)\n \
* Arch: `pacman -S shaderc` (provides glslc)\n \
* Or set GLSLC=/path/to/binary or GLSLANG_VALIDATOR=/path/to/binary.\n"
);
}
fn env_path(name: &str) -> Option<PathBuf> {
let v = std::env::var_os(name)?;
if v.is_empty() {
return None;
}
let p = PathBuf::from(v);
if p.is_file() {
Some(p)
} else {
None
}
}
fn which(binary: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let candidate = dir.join(binary);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
fn compile(compiler: &Compiler, src: &Path, out_dir: &Path) {
let src_str = src.to_string_lossy();
let (stage_long, stage_short) = if src_str.ends_with(".vert.glsl") {
("vertex", "vert")
} else if src_str.ends_with(".frag.glsl") {
("fragment", "frag")
} else {
panic!("unrecognised GLSL stage suffix in {src_str}");
};
let stem = src
.file_name()
.expect("source path has a file name")
.to_string_lossy();
let spv_name = stem
.strip_suffix(".glsl")
.expect("source ends in .glsl")
.to_string()
+ ".spv";
let dst = out_dir.join(&spv_name);
let output = match compiler {
Compiler::Glslc(bin) => Command::new(bin)
.arg(format!("-fshader-stage={stage_long}"))
.arg(src)
.arg("-o")
.arg(&dst)
.output(),
Compiler::Glslang(bin) => Command::new(bin)
.arg("-V")
.arg("-S")
.arg(stage_short)
.arg(src)
.arg("-o")
.arg(&dst)
.output(),
}
.unwrap_or_else(|e| panic!("failed to invoke {compiler:?} on {src_str}: {e}"));
if !output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
panic!(
"GLSL compile failed for {src_str}:\n--stdout--\n{stdout}\n--stderr--\n{stderr}"
);
}
}