use std::collections::HashSet;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
fn main() {
println!("cargo:rerun-if-changed=src/gba/bios/bios.bin");
let root = Path::new("roms/games/mappers");
emit_rerun_hints(root);
let mut autorun_files = Vec::new();
collect_autorun_files(root, &mut autorun_files);
autorun_files.sort();
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by Cargo"));
let generated_path = out_dir.join("autorun_generated_tests.rs");
let mut code = String::new();
code.push_str("// @generated by build.rs\n");
code.push_str("// Do not edit manually.\n\n");
if autorun_files.is_empty() {
code.push_str("#[test]\n");
code.push_str("fn test_no_mapper_autorun_files_found() {\n");
code.push_str(" println!(\"[autorun verification] Skipping - no .autorun files found under roms/games/mappers\");\n");
code.push_str("}\n");
} else {
let mut seen_names = HashSet::new();
for path in autorun_files {
let rel = path_to_unix_string(&path);
let fn_name = unique_test_name(&path, &mut seen_names);
let escaped_rel = escape_for_rust_string(&rel);
code.push_str("#[test]\n");
code.push_str(&format!("fn {fn_name}() {{\n"));
code.push_str(&format!(
" verify_single_autorun(std::path::Path::new(\"{escaped_rel}\"))\n"
));
code.push_str(" .unwrap_or_else(|e| panic!(\"{e}\"));\n");
code.push_str("}\n\n");
}
}
fs::write(&generated_path, code).expect("write generated autorun tests");
}
fn emit_rerun_hints(path: &Path) {
println!("cargo:rerun-if-changed={}", path_to_unix_string(path));
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
emit_rerun_hints(&entry.path());
}
}
}
fn collect_autorun_files(dir: &Path, files: &mut Vec<PathBuf>) {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_autorun_files(&path, files);
} else if path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("autorun"))
{
files.push(path);
}
}
}
fn unique_test_name(path: &Path, seen: &mut HashSet<String>) -> String {
let mapper = mapper_from_path(path);
let game_name = game_name_from_path(path);
let candidate = format!("test_mapper_{mapper}_autorun_{game_name}");
if seen.insert(candidate.clone()) {
return candidate;
}
let mut i = 2usize;
loop {
let with_suffix = format!("{candidate}_{i}");
if seen.insert(with_suffix.clone()) {
return with_suffix;
}
i += 1;
}
}
fn mapper_from_path(path: &Path) -> String {
path.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(|s| sanitize_identifier(s, "unknown"))
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".to_string())
}
fn game_name_from_path(path: &Path) -> String {
let stem = path.file_stem().and_then(|n| n.to_str()).unwrap_or("game");
let simplified = remove_trailing_metadata_groups(stem);
sanitize_identifier(&simplified, "game")
}
fn remove_trailing_metadata_groups(input: &str) -> String {
let mut result = input.trim().to_string();
loop {
let trimmed = result.trim_end();
if trimmed.ends_with(')')
&& let Some(start) = trimmed.rfind('(')
&& trimmed[start..].find(')').is_some()
&& trimmed[start + 1..trimmed.len() - 1]
.chars()
.all(|c| c != '(')
{
result = trimmed[..start].trim_end().to_string();
continue;
}
if trimmed.ends_with(']')
&& let Some(start) = trimmed.rfind('[')
&& trimmed[start..].find(']').is_some()
&& trimmed[start + 1..trimmed.len() - 1]
.chars()
.all(|c| c != '[')
{
result = trimmed[..start].trim_end().to_string();
continue;
}
break;
}
if result.is_empty() {
input.trim().to_string()
} else {
result
}
}
fn sanitize_identifier(text: &str, fallback: &str) -> String {
let mut out = String::new();
for ch in text.chars() {
if ch.is_ascii_alphanumeric() {
out.push(ch.to_ascii_lowercase());
} else {
out.push('_');
}
}
while out.contains("__") {
out = out.replace("__", "_");
}
let out = out.trim_matches('_').to_string();
if out.is_empty() {
fallback.to_string()
} else {
out
}
}
fn path_to_unix_string(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
fn escape_for_rust_string(text: &str) -> String {
text.replace('\\', "\\\\").replace('"', "\\\"")
}