Files
charybdis/build.rs
T
Guillaume GRABÉ 1302ce412c
CI / Format (push) Successful in 48s
CI / Test (push) Failing after 7m9s
CI / Check (push) Successful in 12m6s
CI / Clippy (push) Successful in 12m10s
fix: format and lint
2026-06-08 23:51:29 +02:00

223 lines
7.2 KiB
Rust

use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
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<Vec<PluginConfig>, Box<dyn std::error::Error>> {
let mut plugins = Vec::new();
let mut current_section = String::new();
let mut current_plugin: Option<HashMap<String, String>> = 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(&current_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(&current_section, plugin_data)
{
plugins.push(plugin);
}
Ok(plugins)
}
fn build_plugin_config(section: &str, data: HashMap<String, String>) -> Option<PluginConfig> {
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::<bool>().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::<u32>().ok())?;
let spec_field_number = data
.get("spec_field_number")
.and_then(|v| v.parse::<u32>().ok())?;
Some(PluginConfig {
name,
enabled,
proto_path,
metadata_field_number,
spec_field_number,
})
}
fn generate_entities_proto(plugins: &[PluginConfig]) -> Result<(), Box<dyn std::error::Error>> {
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()
}