use std::{collections::HashSet, collections::HashMap, env, fmt::Write, fs, path::Path, path::MAIN_SEPARATOR};
use walkdir::WalkDir;
use build_print::info;
type Res = Result<(), Box<dyn std::error::Error>>;
fn main() -> Res {
let mut protos = vec![];
let mut pkgs = HashSet::new();
let proto_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("proto");
info!("Proto path: {:?}", &proto_path);
for entry in WalkDir::new(&proto_path)
.into_iter()
.filter_map(Result::ok)
.filter(|e| {
let path_str = e.path().to_str().unwrap();
(
path_str.contains(&format!("googleads{}v18", std::path::MAIN_SEPARATOR))
|| path_str.contains(&format!("google{}rpc", std::path::MAIN_SEPARATOR))
|| path_str.contains(&format!("google{}longrunning", std::path::MAIN_SEPARATOR))
) && e
.path()
.extension()
.map_or(false, |ext| ext == "proto")
})
{
let path = entry.path();
protos.push(path.to_owned());
let content = fs::read_to_string(path).expect("Failed to read proto file");
let pkg = content
.lines()
.find(|line| line.starts_with("package "))
.expect("Package declaration not found")
.split("//")
.next()
.unwrap()
.trim()
.trim_start_matches("package ")
.trim_end_matches(';');
pkgs.insert(pkg.to_string());
}
if protos.is_empty() {
return Err("No .proto files found".into());
} else {
info!("Number of proto files: {}", protos.len());
}
let package_names = ["common", "enums", "errors", "resources", "services"];
let mut protos_by_package: HashMap<&str, Vec<_>> = HashMap::new();
let mut misc_protos: Vec<_> = Vec::new();
for proto in &protos {
let path_str = proto.to_str().unwrap();
let mut matched = false;
for &package in &package_names {
let pkg_str = format!("{MAIN_SEPARATOR}{package}{MAIN_SEPARATOR}");
if path_str.contains(&pkg_str) {
protos_by_package.entry(package).or_insert_with(Vec::new).push(proto.clone());
matched = true;
break;
}
}
if !matched {
misc_protos.push(proto.clone());
}
}
if !misc_protos.is_empty() {
info!("> Compiling {} misc proto files", misc_protos.len());
tonic_build::configure()
.build_server(false)
.compile(&misc_protos, &[proto_path.clone()])?;
}
for &package in &package_names {
if let Some(protos) = protos_by_package.get(package) {
info!("> Compiling {} proto files from package '{}'", protos.len(), package);
for chunk in protos.chunks(185) {
info!(" Compiling batch of {} proto files from package '{}'", chunk.len(), package);
tonic_build::configure()
.build_server(false)
.compile(chunk, &[proto_path.clone()])?;
}
}
}
write_protos_rs(pkgs)?;
Ok(())
}
fn write_protos_rs(pkgs: HashSet<String>) -> Res {
let protos_rs = &mut String::new();
let mut packages: Vec<String> = pkgs.into_iter().collect();
packages.sort();
let mut path_stack: Vec<String> = vec![];
for pkg in packages {
let pop_to = pkg
.split('.')
.map(map_keyword)
.enumerate()
.position(|(idx, pkg_seg)| {
path_stack
.get(idx)
.map_or(true, |stack_seg| stack_seg != &pkg_seg)
})
.unwrap_or(0);
while path_stack.len() > pop_to {
path_stack.pop();
writeln!(protos_rs, "}}")?;
}
for seg in pkg.split('.').skip(pop_to).map(map_keyword) {
writeln!(protos_rs, "pub mod {} {{", &seg)?;
path_stack.push(seg);
}
writeln!(
protos_rs,
"tonic::include_proto!(\"{}\");",
path_stack.join(".")
)?;
}
while !path_stack.is_empty() {
path_stack.pop();
writeln!(protos_rs, "}}").unwrap();
}
let out_dir = env::var("OUT_DIR").expect("OUT_DIR environment variable not set");
fs::write(Path::new(&out_dir).join("protos.rs"), protos_rs)?;
Ok(())
}
fn map_keyword(kw: &str) -> String {
let mut ident = kw.to_string();
match ident.as_str() {
"as" | "break" | "const" | "continue" | "else" | "enum" | "false"
| "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match" | "mod" | "move" | "mut"
| "pub" | "ref" | "return" | "static" | "struct" | "trait" | "true"
| "type" | "unsafe" | "use" | "where" | "while"
| "dyn"
| "abstract" | "become" | "box" | "do" | "final" | "macro" | "override" | "priv" | "typeof"
| "unsized" | "virtual" | "yield"
| "async" | "await" | "try" => ident.insert_str(0, "r#"),
"self" | "super" | "extern" | "crate" => ident += "_",
_ => (),
}
ident
}