robonix_codegen/codegen/
contract_proto_modules_gen.rs1use anyhow::{Context, Result};
6use std::collections::BTreeSet;
7use std::fmt::Write as _;
8use std::fs;
9use std::path::Path;
10
11fn parse_proto_imports(path: &Path) -> Result<Vec<String>> {
13 let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
14 let mut v = Vec::new();
15 for line in text.lines() {
16 let line = line.trim();
17 let Some(rest) = line.strip_prefix("import ") else {
18 continue;
19 };
20 let rest = rest.trim();
21 let Some(rest) = rest.strip_prefix('"') else {
22 continue;
23 };
24 let path_part = rest.split('"').next().unwrap_or("").trim();
25 if path_part.starts_with("google/") {
26 continue;
27 }
28 if path_part.ends_with(".proto") {
29 v.push(path_part.to_string());
30 }
31 }
32 Ok(v)
33}
34
35fn parse_proto_package(path: &Path) -> Result<Option<String>> {
37 let text = fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
38 for line in text.lines() {
39 let line = line.trim();
40 if line.starts_with("//") || line.is_empty() {
41 continue;
42 }
43 let Some(rest) = line.strip_prefix("package ") else {
44 continue;
45 };
46 let pkg = rest.trim_end_matches(';').trim();
47 if !pkg.is_empty() {
48 return Ok(Some(pkg.to_string()));
49 }
50 }
51 Ok(None)
52}
53
54fn package_to_rust_module(dot_pkg: &str) -> String {
56 dot_pkg
57 .rsplit_once('.')
58 .map(|(_, last)| last.to_string())
59 .unwrap_or_else(|| dot_pkg.to_string())
60}
61
62fn dfs_postorder(
63 out_dir: &Path,
64 file: &str,
65 visiting: &mut BTreeSet<String>,
66 visited: &mut BTreeSet<String>,
67 order: &mut Vec<String>,
68) -> Result<()> {
69 if visited.contains(file) {
70 return Ok(());
71 }
72 if visiting.contains(file) {
73 return Ok(()); }
75 let path = out_dir.join(file);
76 if !path.is_file() {
77 return Ok(());
78 }
79
80 visiting.insert(file.to_string());
81 for imp in parse_proto_imports(&path)? {
82 dfs_postorder(out_dir, &imp, visiting, visited, order)?;
83 }
84 visiting.remove(file);
85 visited.insert(file.to_string());
86 order.push(file.to_string());
87 Ok(())
88}
89
90pub fn write(out_dir: &Path, verbose: bool) -> Result<()> {
92 let root = "robonix_contracts.proto";
93 let contracts_path = out_dir.join(root);
94 if !contracts_path.is_file() {
95 return Ok(());
96 }
97
98 let mut visiting = BTreeSet::new();
99 let mut visited = BTreeSet::new();
100 let mut order = Vec::new();
101 dfs_postorder(out_dir, root, &mut visiting, &mut visited, &mut order)?;
102
103 let mut out = String::new();
104 writeln!(
105 &mut out,
106 "// @generated by robonix-codegen (--contracts). Source: proto import closure for `{}`.",
107 root
108 )?;
109 writeln!(&mut out, "// Do not edit by hand.")?;
110 writeln!(
111 &mut out,
112 "// `tonic::include_proto!` order matches prost `super::` sibling resolution."
113 )?;
114 writeln!(
115 &mut out,
116 "// Most generated types are unused in any single binary."
117 )?;
118 writeln!(
119 &mut out,
120 "// (Suppress dead_code on the `robonix-interfaces` crate — not `#![allow]` here: included via `include!`.)"
121 )?;
122 writeln!(&mut out)?;
123
124 for proto_file in &order {
125 let path = out_dir.join(proto_file);
126 let Some(dot_pkg) = parse_proto_package(&path)? else {
127 continue;
128 };
129 let rust_mod = package_to_rust_module(&dot_pkg);
130 writeln!(&mut out, "pub mod {rust_mod} {{")?;
131 writeln!(&mut out, " tonic::include_proto!(\"{dot_pkg}\");")?;
132 writeln!(&mut out, "}}")?;
133 writeln!(&mut out)?;
134 }
135
136 let outfile = out_dir.join("contract_proto_modules.rs");
137 fs::write(&outfile, &out).with_context(|| format!("write {}", outfile.display()))?;
138 if verbose {
139 eprintln!(
140 "[robonix-codegen] contracts: wrote {} ({} include_proto modules)",
141 outfile.display(),
142 order.len()
143 );
144 }
145 Ok(())
146}