rosace_asset_codegen/
lib.rs1use std::collections::HashSet;
24use std::fmt::Write as _;
25use std::fs;
26use std::path::{Path, PathBuf};
27
28pub fn generate(assets_dir: impl AsRef<Path>) {
32 generate_with(assets_dir, "rosace::asset::Asset");
33}
34
35pub fn generate_with(assets_dir: impl AsRef<Path>, asset_type_path: &str) {
38 let dir = assets_dir.as_ref();
39 println!("cargo:rerun-if-changed={}", dir.display());
40
41 let out = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR set by cargo"))
42 .join("rosace_assets.rs");
43
44 let mut body = String::new();
45 if dir.is_dir() {
46 emit_dir(dir, dir, asset_type_path, 0, &mut body);
47 }
48 fs::write(&out, body).expect("write generated assets module");
50}
51
52fn emit_dir(root: &Path, dir: &Path, ty: &str, depth: usize, body: &mut String) {
56 let indent = " ".repeat(depth);
57
58 let mut entries: Vec<PathBuf> = match fs::read_dir(dir) {
60 Ok(rd) => rd.flatten().map(|e| e.path()).collect(),
61 Err(_) => return,
62 };
63 entries.sort();
64
65 let mut used_consts: HashSet<String> = HashSet::new();
66 let mut used_mods: HashSet<String> = HashSet::new();
67
68 for path in &entries {
69 let file_name = match path.file_name().and_then(|n| n.to_str()) {
70 Some(n) => n,
71 None => continue,
72 };
73 if file_name.starts_with('.') {
75 continue;
76 }
77
78 if path.is_dir() {
79 let mod_name = unique(&mod_ident(file_name), &mut used_mods);
80 let _ = writeln!(body, "{indent}#[allow(non_snake_case)]");
81 let _ = writeln!(body, "{indent}pub mod {mod_name} {{");
82 emit_dir(root, path, ty, depth + 1, body);
83 let _ = writeln!(body, "{indent}}}");
84 } else {
85 let rel = path
87 .strip_prefix(root)
88 .unwrap_or(path)
89 .to_string_lossy()
90 .replace('\\', "/");
91 let const_name = unique(&const_ident(file_name), &mut used_consts);
92 let _ = writeln!(
93 body,
94 "{indent}/// `{rel}`\n{indent}pub const {const_name}: {ty} = {ty}::new(\"{rel}\");"
95 );
96 }
97 }
98}
99
100fn const_ident(file_name: &str) -> String {
104 let stem = file_name.rsplit_once('.').map(|(s, _)| s).unwrap_or(file_name);
105 sanitize(stem, true)
106}
107
108fn mod_ident(dir_name: &str) -> String {
110 sanitize(dir_name, false)
111}
112
113fn sanitize(s: &str, upper: bool) -> String {
114 let mut out = String::new();
115 for ch in s.chars() {
116 if ch.is_ascii_alphanumeric() {
117 out.push(if upper { ch.to_ascii_uppercase() } else { ch.to_ascii_lowercase() });
118 } else {
119 out.push('_');
120 }
121 }
122 if out.is_empty() {
123 out.push('_');
124 }
125 if out.chars().next().map(|c| c.is_ascii_digit()).unwrap_or(false) {
126 out.insert(0, '_');
127 }
128 out
129}
130
131fn unique(base: &str, used: &mut HashSet<String>) -> String {
134 if used.insert(base.to_string()) {
135 return base.to_string();
136 }
137 let mut n = 2;
138 loop {
139 let candidate = format!("{base}_{n}");
140 if used.insert(candidate.clone()) {
141 return candidate;
142 }
143 n += 1;
144 }
145}