use std::collections::HashMap; use std::fs; use std::path::PathBuf; fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=plugins.toml"); println!("cargo:rerun-if-changed=proto/entities.proto.template"); println!("cargo:rerun-if-changed=proto/core/"); println!("cargo:rerun-if-changed=proto/ingestion.proto"); println!("cargo:rerun-if-changed=plugins/"); // Parse plugins.toml to get enabled plugins let plugins_config = fs::read_to_string("plugins.toml").expect("Failed to read plugins.toml"); let plugins = parse_plugins_config(&plugins_config)?; // Generate entities.proto from template generate_entities_proto(&plugins)?; // Compile all proto files let out_dir = PathBuf::from(std::env::var("OUT_DIR")?); // Collect all proto files to compile let mut proto_files = vec!["proto/entities.proto".to_string()]; // Add core proto files proto_files.push("proto/core/service.proto".to_string()); proto_files.push("proto/core/system.proto".to_string()); proto_files.push("proto/core/component.proto".to_string()); proto_files.push("proto/core/api.proto".to_string()); proto_files.push("proto/core/user.proto".to_string()); proto_files.push("proto/core/group.proto".to_string()); proto_files.push("proto/core/domain.proto".to_string()); proto_files.push("proto/core/resource.proto".to_string()); proto_files.push("proto/core/finding.proto".to_string()); proto_files.push("proto/ingestion.proto".to_string()); // Add enabled plugin proto files for plugin in &plugins { if plugin.enabled { let proto_path = format!("{}/{}.proto", plugin.proto_path, plugin.name); if std::path::Path::new(&proto_path).exists() { proto_files.push(proto_path.clone()); println!("cargo:rerun-if-changed={}", proto_path); } } } // Compile protos with tonic (0.14+ uses tonic-prost-build) let includes = vec!["proto".to_string(), ".".to_string()]; tonic_prost_build::configure() .file_descriptor_set_path(out_dir.join("entity_descriptor.bin")) .compile_protos(&proto_files, &includes)?; println!( "✓ Generated entities.proto with {} plugins", plugins.iter().filter(|p| p.enabled).count() ); Ok(()) } #[derive(Debug)] struct PluginConfig { name: String, enabled: bool, proto_path: String, metadata_field_number: u32, spec_field_number: u32, } fn parse_plugins_config(config: &str) -> Result, Box> { let mut plugins = Vec::new(); let mut current_section = String::new(); let mut current_plugin: Option> = None; for line in config.lines() { let line = line.trim(); // Skip comments and empty lines if line.is_empty() || line.starts_with('#') { continue; } // Section header [plugins.name] if line.starts_with('[') && line.ends_with(']') { // Save previous plugin if exists if let Some(plugin_data) = current_plugin.take() && let Some(plugin) = build_plugin_config(¤t_section, plugin_data) { plugins.push(plugin); } current_section = line[1..line.len() - 1].to_string(); // Start collecting data for plugins.* sections if current_section.starts_with("plugins.") { current_plugin = Some(HashMap::new()); } continue; } // Key = value if let Some(eq_pos) = line.find('=') { let key = line[..eq_pos].trim(); let value = line[eq_pos + 1..] .trim() .trim_matches('"') .trim_matches('\''); if let Some(ref mut plugin_data) = current_plugin { plugin_data.insert(key.to_string(), value.to_string()); } } } // Don't forget the last plugin if let Some(plugin_data) = current_plugin.take() && let Some(plugin) = build_plugin_config(¤t_section, plugin_data) { plugins.push(plugin); } Ok(plugins) } fn build_plugin_config(section: &str, data: HashMap) -> Option { if !section.starts_with("plugins.") { return None; } let name = section.strip_prefix("plugins.")?.to_string(); let enabled = data .get("enabled") .and_then(|v| v.parse::().ok()) .unwrap_or(false); let proto_path = data.get("proto_path")?.clone(); let metadata_field_number = data .get("metadata_field_number") .and_then(|v| v.parse::().ok())?; let spec_field_number = data .get("spec_field_number") .and_then(|v| v.parse::().ok())?; Some(PluginConfig { name, enabled, proto_path, metadata_field_number, spec_field_number, }) } fn generate_entities_proto(plugins: &[PluginConfig]) -> Result<(), Box> { let template = fs::read_to_string("proto/entities.proto.template") .expect("Failed to read proto/entities.proto.template"); // Generate import statements for enabled plugins let mut imports = String::new(); for plugin in plugins { if plugin.enabled { let import_line = format!( "import \"plugins/{}/proto/{}.proto\";\n", plugin.name, plugin.name ); imports.push_str(&import_line); } } // Generate metadata oneof fields for enabled plugins let mut metadata_fields = String::new(); for plugin in plugins { if plugin.enabled { // Convert plugin name to PascalCase for message type let pascal_name = to_pascal_case(&plugin.name); let field_line = format!( " charybdis.plugins.{}.{}Metadata {}_metadata = {};\n", plugin.name, pascal_name, plugin.name, plugin.metadata_field_number ); metadata_fields.push_str(&field_line); } } // Generate spec oneof fields for enabled plugins let mut spec_fields = String::new(); for plugin in plugins { if plugin.enabled { let pascal_name = to_pascal_case(&plugin.name); let field_line = format!( " charybdis.plugins.{}.{}Spec {}_spec = {};\n", plugin.name, pascal_name, plugin.name, plugin.spec_field_number ); spec_fields.push_str(&field_line); } } // Replace placeholders in template let mut output = template.replace("{{PLUGIN_IMPORTS}}", &imports); output = output.replace("{{PLUGIN_METADATA_FIELDS}}", &metadata_fields); output = output.replace("{{PLUGIN_SPEC_FIELDS}}", &spec_fields); // Write generated file fs::write("proto/entities.proto", output).expect("Failed to write proto/entities.proto"); Ok(()) } fn to_pascal_case(s: &str) -> String { s.split('_') .map(|word| { let mut chars = word.chars(); match chars.next() { None => String::new(), Some(first) => first.to_uppercase().chain(chars).collect(), } }) .collect() }