use crate::assemble::{Stage, detect_stage};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub fn shared_libraries(dir: &Path, stage: Stage, target: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut files: Vec<PathBuf> = entries.flatten().map(|e| e.path()).collect();
files.sort();
let mut out = Vec::new();
let mut seen_fns: HashSet<String> = HashSet::new();
for p in files {
if p.extension().and_then(|e| e.to_str()) != Some("glsl") {
continue;
}
if same_path(&p, target) {
continue;
}
if detect_stage(&p).is_some_and(|s| s != stage) {
continue;
}
let Ok(content) = std::fs::read_to_string(&p) else {
continue;
};
if has_main(&content) {
continue; }
let fns = top_level_fn_names(&content);
if fns.iter().any(|f| seen_fns.contains(f)) {
continue;
}
seen_fns.extend(fns);
out.push(p);
}
out
}
pub fn injected_defines(start: &Path) -> Vec<String> {
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<HashMap<PathBuf, Vec<String>>>> = OnceLock::new();
let root = repo_root(start);
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Ok(guard) = cache.lock()
&& let Some(hit) = guard.get(&root)
{
return hit.clone();
}
let mut names: HashSet<String> = HashSet::new();
scan_defines(&root, &mut names, 0);
let mut out: Vec<String> = names.into_iter().collect();
out.sort();
if let Ok(mut guard) = cache.lock() {
guard.insert(root, out.clone());
}
out
}
fn scan_defines(dir: &Path, out: &mut HashSet<String>, depth: usize) {
if depth > 12 {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
let name = p.file_name().and_then(|n| n.to_str()).unwrap_or("");
if p.is_dir() {
if matches!(name, "node_modules" | ".git" | "target" | "dist" | "build") {
continue;
}
scan_defines(&p, out, depth + 1);
} else if matches!(
p.extension().and_then(|x| x.to_str()),
Some("ts" | "tsx" | "js" | "jsx" | "mjs" | "cjs") ) && !name.ends_with(".glsl.g.ts")
&& !name.ends_with(".glsl.js")
&& let Ok(text) = std::fs::read_to_string(&p)
{
collect_define_names(&text, out);
}
}
}
fn collect_define_names(text: &str, out: &mut HashSet<String>) {
for (i, _) in text.match_indices("#define ") {
let rest = &text[i + "#define ".len()..];
let name: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
let after = &rest[name.len()..];
if name.len() >= 2
&& name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
&& after.trim_start().starts_with("${")
&& after.starts_with([' ', '\t'])
{
out.insert(name);
}
}
}
fn repo_root(start: &Path) -> PathBuf {
let mut dir = Some(start);
while let Some(d) = dir {
if d.join(".git").exists() {
return d.to_path_buf();
}
dir = d.parent();
}
start.to_path_buf()
}
fn has_main(source: &str) -> bool {
source
.lines()
.any(|l| l.replace(char::is_whitespace, "").contains("voidmain("))
}
fn top_level_fn_names(source: &str) -> Vec<String> {
let mut out = Vec::new();
for line in source.lines() {
if line.starts_with([' ', '\t']) || line.is_empty() {
continue;
}
let Some(paren) = line.find('(') else {
continue;
};
let head = &line[..paren];
let mut toks = head.split_whitespace();
let (Some(ret), Some(name)) = (toks.next(), toks.last()) else {
continue;
};
if is_ident(ret)
&& is_ident(name)
&& !matches!(name, "if" | "for" | "while" | "switch" | "return")
{
out.push(name.to_string());
}
}
out
}
fn is_ident(s: &str) -> bool {
let mut cs = s.chars();
cs.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn same_path(a: &Path, b: &Path) -> bool {
match (a.canonicalize(), b.canonicalize()) {
(Ok(x), Ok(y)) => x == y,
_ => a == b,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn top_level_fn_names_finds_definitions_not_calls() {
let src = "\
uniform mat4 u_m;
vec4 projectTile(vec2 p) {
vec4 r = u_m * vec4(p, 0.0, 1.0);
if (p.x > 0.0) { r.z = 1.0; }
return r;
}
float projectLineThickness(float y) { return 1.0; }
";
let fns = top_level_fn_names(src);
assert!(fns.contains(&"projectTile".to_string()));
assert!(fns.contains(&"projectLineThickness".to_string()));
assert!(!fns.iter().any(|f| f == "if" || f == "u_m" || f == "vec4"));
}
#[test]
fn collect_define_names_skips_templated_macros() {
let mut out = HashSet::new();
collect_define_names(
"const d = [`#define NUM_ILLUMINATION_SOURCES ${x.length}`, `#define HAS_UNIFORM_${name}`];",
&mut out,
);
assert!(out.contains("NUM_ILLUMINATION_SOURCES"));
assert!(!out.iter().any(|n| n.starts_with("HAS_UNIFORM")));
}
}