use std::path::{Path, PathBuf}; use std::env; use std::fs; use std::str; use glob::glob; use regex::Regex; use std::collections::HashMap; #[derive(Clone, Debug)] struct ModuleInfo { name: String, version: String, path: String, } fn check_unstable(proto_path: &PathBuf, build_unstable: bool) -> bool { if build_unstable { return true; } let proto_path_str: &str = proto_path.to_str().unwrap(); return !(proto_path_str.contains("alpha/") || proto_path_str.contains("beta/") || proto_path_str.contains("dev/")); } fn find_proto_files(proto_dir: &str, build_unstable: bool) -> impl Iterator { let proto_pattern = format!("{}/**/*.proto", proto_dir); match glob(&proto_pattern) { Ok(iter) => iter .map(|item| item.expect("Unable to read file")) .filter(move |item| check_unstable(item, build_unstable)) .map(|item| item.to_owned()), Err(err) => panic!( "Unable to read proto directory {}: {:?}", proto_dir, err ) } } fn main() -> Result<(), Box> { let proto_dir = "./proto/sentry_protos"; println!("Generating protos in {}", proto_dir); let build_unstable = match env::var("SENTRY_PROTOS_BUILD_UNSTABLE") { Ok(v) => v == "1", Err(_) => false, }; let mut build_mode = "stable"; if build_unstable { build_mode = "unstable"; } println!("Building {build_mode} protos"); // collect module names to generate lib.rs let mut module_metadata = Vec::new(); let mut proto_files: Vec = Vec::new(); for file in find_proto_files(proto_dir, build_unstable) { module_metadata.push(get_module_info(&file)); proto_files.push(file); } // Compile rust code for all proto files. // You can use .out_dir("./src") to generate code into files for local inspection. println!("Generating proto bindings"); tonic_build::configure() .emit_rerun_if_changed(false) .compile(&proto_files, &["./proto"]) .unwrap(); let mut visited: Vec<&str> = vec![]; let mut code = String::new(); use std::fmt::Write; let mut module_map = HashMap::<&str, Vec<&ModuleInfo>>::new(); for module in module_metadata.iter() { module_map.entry(&module.name).and_modify(|e| {e.push(module)}).or_insert(vec![module]); } for (module_name, modules) in module_map.iter() { writeln!(code, "pub mod {module_name} {{").unwrap(); for module in modules.iter() { if visited.iter().any(|i| i.contains(&module.path)) { continue; } visited.push(module.path.as_str()); let module_version = &module.version; let module_path = &module.path; writeln!(code, " pub mod {module_version} {{").unwrap(); writeln!(code, " tonic::include_proto!(\"{module_path}\");").unwrap(); writeln!(code, " }}").unwrap(); } writeln!(code, "}}").unwrap(); writeln!(code, "").unwrap(); } // Generate lib.rs with the proto modules. println!("Generating src/lib.rs"); std::fs::write( Path::new("./src/lib.rs"), code ) .expect("Failed to write lib.rs"); // Once protos are built, layer in client adapters. Ok(()) } fn get_module_info(path: &PathBuf) -> ModuleInfo { let file = fs::read(path); let Ok(contents) = file else { panic!("Could not read {:?}", path); }; let contents_str = str::from_utf8(&contents).unwrap(); let pattern = Regex::new(r"(?m)^package\s+(?[^;]+);").unwrap(); let Some(captures) = pattern.captures(contents_str) else { panic!("Could not find package name in {:?}", path); }; let package_name = captures["name"].to_string(); let mut parts = package_name.split('.'); let version = parts.nth_back(0).unwrap().to_string(); let name = parts.nth_back(0).unwrap().to_string(); ModuleInfo {name, version, path: package_name} }