use std::env;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
const INCLUDE_ROOTS: &[&str] = &[
"AGENTS.md",
"CLAUDE.md",
".agents",
".claude",
".codex",
".cursor",
".devin",
".kiro",
".opencode",
".trae",
"bot",
"plugins",
"schemas",
"templates",
"docs",
"clients",
"presets",
"adapters",
"tests",
"examples",
".github/workflows/sdd.yml",
"VERSION",
"CHANGELOG.md",
"sdd.config.example.yaml",
];
const BOT_EMBED_EXCLUDES: &[&str] = &["node_modules", "dist"];
fn main() -> io::Result<()> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR"));
let generated = out_dir.join("embedded.rs");
let mut files = Vec::new();
println!("cargo:rerun-if-changed=build.rs");
for rel in INCLUDE_ROOTS {
collect_files(&manifest_dir, Path::new(rel), &mut files)?;
}
files.sort();
let mut output = String::from("pub const EMBEDDED_FILES: &[(&str, &[u8])] = &[\n");
for rel in files {
let abs = manifest_dir.join(&rel);
output.push_str(&format!(
" ({rel:?}, include_bytes!({abs:?})),\n",
rel = rel.to_string_lossy().replace('\\', "/"),
abs = abs.to_string_lossy()
));
}
output.push_str("];\n");
let mut file = fs::File::create(generated)?;
file.write_all(output.as_bytes())?;
Ok(())
}
fn collect_files(root: &Path, rel: &Path, files: &mut Vec<PathBuf>) -> io::Result<()> {
let path = root.join(rel);
if !path.exists() {
return Ok(());
}
if ignored(rel) {
return Ok(());
}
println!("cargo:rerun-if-changed={}", path.display());
if is_generated_docs_dir(rel, &path) {
return Ok(());
}
if path.is_file() {
files.push(rel.to_path_buf());
return Ok(());
}
for entry in fs::read_dir(path)? {
let entry = entry?;
collect_files(root, &rel.join(entry.file_name()), files)?;
}
Ok(())
}
fn ignored(rel: &Path) -> bool {
let normalized = rel.to_string_lossy().replace('\\', "/");
if normalized == ".mcp.json" || normalized == ".cursor/mcp.json" {
return true;
}
if is_agent_worktree_state(rel) {
return true;
}
if is_bot_generated(rel) {
return true;
}
rel.components().any(|component| {
let name = component.as_os_str().to_string_lossy();
matches!(name.as_ref(), ".DS_Store" | ".git" | ".sdd" | "target")
})
}
fn is_bot_generated(rel: &Path) -> bool {
let mut components = rel
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned());
if components.next().as_deref() != Some("bot") {
return false;
}
if let Some(second) = components.next() {
return BOT_EMBED_EXCLUDES.contains(&second.as_str());
}
false
}
fn is_agent_worktree_state(rel: &Path) -> bool {
let mut components = rel
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned());
matches!(
(components.next().as_deref(), components.next().as_deref()),
(Some(".claude"), Some("worktrees"))
)
}
fn is_generated_docs_dir(rel: &Path, path: &Path) -> bool {
if !path.is_dir() {
return false;
}
let components: Vec<_> = rel.components().collect();
components.len() == 2 && components[0].as_os_str() == "docs"
}