use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
fn main() {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
let builtins_dir = manifest_dir.join("src/engine/builtins");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
generate_map(
&builtins_dir.join("steps"),
"md",
"BUILTIN_STEPS",
&out_dir.join("builtin_steps.rs"),
);
generate_map(
&builtins_dir.join("flows"),
"yaml",
"BUILTIN_FLOWS",
&out_dir.join("builtin_flows.rs"),
);
generate_flow_categories(
&builtins_dir.join("flows"),
&out_dir.join("builtin_flow_categories.rs"),
);
generate_map(
&builtins_dir.join("directions"),
"md",
"BUILTIN_DIRECTIONS",
&out_dir.join("builtin_directions.rs"),
);
assert_unique_direction_node_names(&builtins_dir.join("directions"));
generate_direction_groups(
&builtins_dir.join("directions"),
&out_dir.join("builtin_direction_groups.rs"),
);
generate_map(
&builtins_dir.join("ops"),
"md",
"BUILTIN_OPS_PROMPTS",
&out_dir.join("builtin_ops_prompts.rs"),
);
println!("cargo:rerun-if-changed={}", builtins_dir.display());
for entry in walkdir(&builtins_dir) {
println!("cargo:rerun-if-changed={}", entry.display());
}
}
fn generate_map(dir: &Path, extension: &str, map_name: &str, out_path: &Path) {
let mut entries: Vec<(String, PathBuf)> = Vec::new();
collect_files(dir, extension, &mut entries);
entries.sort_by(|a, b| a.0.cmp(&b.0));
for pair in entries.windows(2) {
if pair[0].0 == pair[1].0 {
panic!(
"duplicate builtin key '{}' for '{}' and '{}'",
pair[0].0,
pair[0].1.display(),
pair[1].1.display()
);
}
}
let mut code = String::new();
writeln!(
code,
"static {map_name}: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> = std::sync::LazyLock::new(|| {{"
)
.expect("write to String");
writeln!(code, " let mut m = std::collections::HashMap::new();").expect("write to String");
for (name, path) in &entries {
let abs = path
.canonicalize()
.unwrap_or_else(|e| panic!("canonicalize {}: {e}", path.display()));
let abs_str = abs.to_string_lossy().replace('\\', "/");
writeln!(
code,
" m.insert(\"{name}\", include_str!(\"{abs_str}\"));"
)
.expect("write to String");
}
writeln!(code, " m").expect("write to String");
writeln!(code, "}});").expect("write to String");
fs::write(out_path, code).unwrap_or_else(|e| panic!("write {}: {e}", out_path.display()));
}
fn generate_flow_categories(flows_dir: &Path, out_path: &Path) {
let mut categories: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
let Ok(entries) = fs::read_dir(flows_dir) else {
fs::write(
out_path,
"pub const BUILTIN_FLOW_CATEGORIES: &[(&str, &[&str])] = &[];\n",
)
.expect("write empty flow categories");
return;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let dir_name = path
.file_name()
.expect("dir has no name")
.to_string_lossy()
.to_string();
let category = title_case(&dir_name);
let mut flows = Vec::new();
if let Ok(files) = fs::read_dir(&path) {
for file in files.flatten() {
let file_path = file.path();
if file_path
.extension()
.is_some_and(|e| e == "yaml" || e == "yml")
{
let name = file_path
.file_stem()
.expect("file has no stem")
.to_string_lossy()
.to_string();
flows.push(name);
}
}
}
flows.sort();
if !flows.is_empty() {
categories.insert(category, flows);
}
}
let mut code = String::new();
writeln!(
code,
"pub const BUILTIN_FLOW_CATEGORIES: &[(&str, &[&str])] = &["
)
.expect("write to String");
for (category, flows) in &categories {
let flow_list: Vec<String> = flows.iter().map(|f| format!("\"{f}\"")).collect();
writeln!(code, " (\"{category}\", &[{}]),", flow_list.join(", "))
.expect("write to String");
}
writeln!(code, "];").expect("write to String");
fs::write(out_path, code).unwrap_or_else(|e| panic!("write {}: {e}", out_path.display()));
}
fn generate_direction_groups(directions_dir: &Path, out_path: &Path) {
let mut groups: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
let Ok(entries) = fs::read_dir(directions_dir) else {
fs::write(
out_path,
"static BUILTIN_DIRECTION_GROUPS: std::sync::LazyLock<std::collections::HashMap<&'static str, Vec<&'static str>>> = std::sync::LazyLock::new(|| std::collections::HashMap::new());\n",
)
.expect("write empty direction groups");
return;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let group_name = path
.file_name()
.expect("group dir has no name")
.to_string_lossy()
.to_string();
let mut members = Vec::new();
if let Ok(files) = fs::read_dir(&path) {
for file in files.flatten() {
let file_path = file.path();
if file_path.extension().is_some_and(|e| e == "md") {
let member = file_path
.file_stem()
.expect("direction file has no stem")
.to_string_lossy()
.to_string();
members.push(member);
}
}
}
members.sort();
if !members.is_empty() {
groups.insert(group_name, members);
}
}
let mut code = String::new();
writeln!(
code,
"static BUILTIN_DIRECTION_GROUPS: std::sync::LazyLock<std::collections::HashMap<&'static str, Vec<&'static str>>> = std::sync::LazyLock::new(|| {{"
)
.expect("write to String");
writeln!(code, " let mut m = std::collections::HashMap::new();").expect("write to String");
for (group, members) in &groups {
let member_list = members
.iter()
.map(|member| format!("\"{member}\""))
.collect::<Vec<_>>()
.join(", ");
writeln!(code, " m.insert(\"{group}\", vec![{member_list}]);").expect("write to String");
}
writeln!(code, " m").expect("write to String");
writeln!(code, "}});").expect("write to String");
fs::write(out_path, code).unwrap_or_else(|e| panic!("write {}: {e}", out_path.display()));
}
fn assert_unique_direction_node_names(directions_dir: &Path) {
let mut leaves: Vec<(String, PathBuf)> = Vec::new();
collect_files(directions_dir, "md", &mut leaves);
let mut groups = Vec::new();
if let Ok(entries) = fs::read_dir(directions_dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = path
.file_name()
.expect("group dir has no name")
.to_string_lossy()
.to_string();
groups.push((name, path));
}
}
let mut origins: std::collections::BTreeMap<String, Vec<String>> =
std::collections::BTreeMap::new();
for (name, path) in groups {
origins
.entry(name)
.or_default()
.push(format!("group {}", path.display()));
}
for (name, path) in leaves {
origins
.entry(name)
.or_default()
.push(format!("direction {}", path.display()));
}
let collisions: Vec<String> = origins
.into_iter()
.filter_map(|(name, paths)| {
if paths.len() > 1 {
Some(format!("{name}: {}", paths.join(", ")))
} else {
None
}
})
.collect();
if !collisions.is_empty() {
panic!(
"duplicate builtin direction node names detected (groups + leaves share one namespace):\n{}",
collisions.join("\n")
);
}
}
fn title_case(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
}
}
fn collect_files(dir: &Path, extension: &str, entries: &mut Vec<(String, PathBuf)>) {
let Ok(read_dir) = fs::read_dir(dir) else {
return;
};
for entry in read_dir {
let Ok(entry) = entry else { continue };
let path = entry.path();
if path.is_dir() {
collect_files(&path, extension, entries);
} else if path.extension().is_some_and(|e| e == extension) {
let name = path
.file_stem()
.expect("file has no stem")
.to_string_lossy()
.to_string();
entries.push((name, path));
}
}
}
fn walkdir(dir: &Path) -> Vec<PathBuf> {
let mut result = Vec::new();
let Ok(read_dir) = fs::read_dir(dir) else {
return result;
};
for entry in read_dir {
let Ok(entry) = entry else { continue };
let path = entry.path();
result.push(path.clone());
if path.is_dir() {
result.extend(walkdir(&path));
}
}
result
}