use std::fs;
use std::io::Write;
use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=../.git/HEAD");
println!("cargo:rerun-if-changed=../.git/refs/");
if let Ok(output) = std::process::Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
.output()
{
if output.status.success() {
let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
let dirty = std::process::Command::new("git")
.args(["status", "--porcelain"])
.output()
.map(|o| !o.stdout.is_empty())
.unwrap_or(false);
let suffix = if dirty {
format!("{}+{}-dirty", env!("CARGO_PKG_VERSION"), hash)
} else {
format!("{}+{}", env!("CARGO_PKG_VERSION"), hash)
};
println!("cargo:rustc-env=CROSSLINK_VERSION={}", suffix);
}
}
println!("cargo:rerun-if-changed=resources/claude/settings.json");
println!("cargo:rerun-if-changed=resources/claude/hooks/prompt-guard.py");
println!("cargo:rerun-if-changed=resources/claude/hooks/post-edit-check.py");
println!("cargo:rerun-if-changed=resources/claude/hooks/session-start.py");
println!("cargo:rerun-if-changed=resources/claude/hooks/pre-web-check.py");
println!("cargo:rerun-if-changed=resources/claude/hooks/work-check.py");
println!("cargo:rerun-if-changed=resources/claude/mcp/safe-fetch-server.py");
println!("cargo:rerun-if-changed=resources/mcp.json");
println!("cargo:rerun-if-changed=resources/claude/commands/workflow.md");
println!("cargo:rerun-if-changed=resources/crosslink/hook-config.json");
println!("cargo:rerun-if-changed=resources/crosslink/rules/");
let rules_dir = Path::new("resources/crosslink/rules");
if rules_dir.is_dir() {
if let Err(e) = generate_rules_file(rules_dir) {
eprintln!("cargo:warning=Failed to generate rules_gen.rs: {}", e);
}
}
println!("cargo:rerun-if-changed=resources/claude/commands/");
let commands_dir = Path::new("resources/claude/commands");
if commands_dir.is_dir() {
if let Err(e) = generate_commands_file(commands_dir) {
eprintln!("cargo:warning=Failed to generate commands_gen.rs: {}", e);
}
}
}
fn generate_commands_file(commands_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut cmd_entries: Vec<(String, String)> = Vec::new();
let mut entries: Vec<_> = fs::read_dir(commands_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(".md"))
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let filename = entry.file_name().to_string_lossy().to_string();
let rel_path = format!("resources/claude/commands/{}", filename);
println!("cargo:rerun-if-changed={}", rel_path);
let const_name = filename
.trim_end_matches(".md")
.to_uppercase()
.replace('-', "_");
let const_name = format!("CMD_{}", const_name);
cmd_entries.push((filename, const_name));
}
let out_dir = std::env::var("OUT_DIR")?;
let gen_path = Path::new(&out_dir).join("commands_gen.rs");
let mut gen_file = fs::File::create(&gen_path)?;
writeln!(
gen_file,
"// Auto-generated by build.rs — do not edit manually"
)?;
writeln!(gen_file, "// Generated from resources/claude/commands/")?;
writeln!(gen_file)?;
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
let abs_commands_dir = Path::new(&manifest_dir).join("resources/claude/commands");
for (filename, const_name) in &cmd_entries {
let abs_path = abs_commands_dir.join(filename);
let abs_path_str = abs_path.to_string_lossy().replace('\\', "/");
writeln!(
gen_file,
"pub(crate) const {}: &str = include_str!(\"{}\");",
const_name, abs_path_str
)?;
}
writeln!(gen_file)?;
writeln!(
gen_file,
"pub(crate) const COMMAND_FILES: &[(&str, &str)] = &["
)?;
for (filename, const_name) in &cmd_entries {
writeln!(gen_file, " (\"{}\", {}),", filename, const_name)?;
}
writeln!(gen_file, "];")?;
Ok(())
}
fn generate_rules_file(rules_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let mut rule_entries: Vec<(String, String)> = Vec::new();
let mut entries: Vec<_> = fs::read_dir(rules_dir)?
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name().to_string_lossy().to_string();
name.ends_with(".md") || name.ends_with(".txt")
})
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let filename = entry.file_name().to_string_lossy().to_string();
let rel_path = format!("resources/crosslink/rules/{}", filename);
println!("cargo:rerun-if-changed={}", rel_path);
let const_name = filename
.trim_end_matches(".md")
.trim_end_matches(".txt")
.to_uppercase()
.replace('-', "_");
let const_name = format!("RULE_{}", const_name);
rule_entries.push((filename, const_name));
}
let out_dir = std::env::var("OUT_DIR")?;
let gen_path = Path::new(&out_dir).join("rules_gen.rs");
let mut gen_file = fs::File::create(&gen_path)?;
writeln!(
gen_file,
"// Auto-generated by build.rs — do not edit manually"
)?;
writeln!(gen_file, "// Generated from resources/crosslink/rules/")?;
writeln!(gen_file)?;
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
let abs_rules_dir = Path::new(&manifest_dir).join("resources/crosslink/rules");
for (filename, const_name) in &rule_entries {
let abs_path = abs_rules_dir.join(filename);
let abs_path_str = abs_path.to_string_lossy().replace('\\', "/");
writeln!(
gen_file,
"pub(crate) const {}: &str = include_str!(\"{}\");",
const_name, abs_path_str
)?;
}
writeln!(gen_file)?;
writeln!(
gen_file,
"pub(crate) const RULE_FILES: &[(&str, &str)] = &["
)?;
for (filename, const_name) in &rule_entries {
writeln!(gen_file, " (\"{}\", {}),", filename, const_name)?;
}
writeln!(gen_file, "];")?;
Ok(())
}