robonix_codegen/codegen/
ros2_gen.rs1use anyhow::{Context, Result};
18use std::collections::{BTreeMap, BTreeSet};
19use std::fmt::Write as FmtWrite;
20use std::fs;
21use std::path::Path;
22
23use super::msg_parser::{MsgResolver, MsgTypeRef};
24
25const PKG_VERSION: &str = "0.1.0";
26const MAINTAINER: &str = "robonix";
27const MAINTAINER_EMAIL: &str = "robonix@syswonder.org";
28const LICENSE: &str = "MulanPSL-2.0";
29
30fn normalize_idl(src: &str) -> String {
44 let mut out = String::with_capacity(src.len());
45 for line in src.lines() {
46 match line.split_once('#') {
47 Some((code, comment)) => {
48 out.push_str(&code.replace("/msg/", "/").replace("/srv/", "/"));
49 out.push('#');
50 out.push_str(&comment.replace("*/", "* /"));
51 }
52 None => out.push_str(&line.replace("/msg/", "/").replace("/srv/", "/")),
53 }
54 out.push('\n');
55 }
56 out
57}
58
59fn package_xml(pkg: &str, deps: &BTreeSet<String>) -> String {
60 let mut s = String::new();
61 let _ = writeln!(s, "<?xml version=\"1.0\"?>");
62 let _ = writeln!(s, "<!-- @generated by robonix-codegen (lang ros2) -->");
64 let _ = writeln!(s, "<package format=\"3\">");
65 let _ = writeln!(s, " <name>{pkg}</name>");
66 let _ = writeln!(s, " <version>{PKG_VERSION}</version>");
67 let _ = writeln!(
68 s,
69 " <description>Robonix canonical ROS 2 interfaces — {pkg} (generated).</description>"
70 );
71 let _ = writeln!(
72 s,
73 " <maintainer email=\"{MAINTAINER_EMAIL}\">{MAINTAINER}</maintainer>"
74 );
75 let _ = writeln!(s, " <license>{LICENSE}</license>");
76 let _ = writeln!(s);
77 let _ = writeln!(s, " <buildtool_depend>ament_cmake</buildtool_depend>");
78 let _ = writeln!(
79 s,
80 " <buildtool_depend>rosidl_default_generators</buildtool_depend>"
81 );
82 let _ = writeln!(s);
83 for d in deps {
84 let _ = writeln!(s, " <depend>{d}</depend>");
85 }
86 if !deps.is_empty() {
87 let _ = writeln!(s);
88 }
89 let _ = writeln!(s, " <exec_depend>rosidl_default_runtime</exec_depend>");
90 let _ = writeln!(
91 s,
92 " <member_of_group>rosidl_interface_packages</member_of_group>"
93 );
94 let _ = writeln!(s);
95 let _ = writeln!(s, " <export>");
96 let _ = writeln!(s, " <build_type>ament_cmake</build_type>");
97 let _ = writeln!(s, " </export>");
98 let _ = writeln!(s, "</package>");
99 s
100}
101
102fn cmakelists(pkg: &str, msgs: &[String], srvs: &[String], deps: &BTreeSet<String>) -> String {
103 let mut s = String::new();
104 let _ = writeln!(s, "# @generated by robonix-codegen --lang ros2");
105 let _ = writeln!(s, "cmake_minimum_required(VERSION 3.8)");
106 let _ = writeln!(s, "project({pkg})");
107 let _ = writeln!(s);
108 let _ = writeln!(s, "find_package(ament_cmake REQUIRED)");
109 let _ = writeln!(s, "find_package(rosidl_default_generators REQUIRED)");
110 for d in deps {
111 let _ = writeln!(s, "find_package({d} REQUIRED)");
112 }
113 let _ = writeln!(s);
114 let _ = writeln!(s, "rosidl_generate_interfaces(${{PROJECT_NAME}}");
115 for m in msgs {
116 let _ = writeln!(s, " \"msg/{m}.msg\"");
117 }
118 for sv in srvs {
119 let _ = writeln!(s, " \"srv/{sv}.srv\"");
120 }
121 if !deps.is_empty() {
122 let joined: Vec<&str> = deps.iter().map(|s| s.as_str()).collect();
123 let _ = writeln!(s, " DEPENDENCIES {}", joined.join(" "));
124 }
125 let _ = writeln!(s, ")");
126 let _ = writeln!(s);
127 let _ = writeln!(s, "ament_package()");
128 s
129}
130
131#[derive(Default)]
133struct PkgPlan {
134 msgs: BTreeSet<String>,
135 srvs: BTreeSet<String>,
136 deps: BTreeSet<String>,
137}
138
139pub fn generate(resolver: &MsgResolver, out_dir: &Path, verbose: bool) -> Result<()> {
140 let mut plans: BTreeMap<String, PkgPlan> = BTreeMap::new();
146 for ((pkg, name), spec) in &resolver.cache {
147 let plan = plans.entry(pkg.clone()).or_default();
148 plan.msgs.insert(name.clone());
149 for f in &spec.fields {
150 if let MsgTypeRef::Named { package, .. } = &f.type_ref
151 && package != pkg
152 {
153 plan.deps.insert(package.clone());
154 }
155 }
156 }
157 for ((pkg, name), srv) in &resolver.srv_cache {
158 let plan = plans.entry(pkg.clone()).or_default();
159 plan.srvs.insert(name.clone());
160 for f in srv.request.fields.iter().chain(srv.response.fields.iter()) {
161 if let MsgTypeRef::Named { package, .. } = &f.type_ref
162 && package != pkg
163 {
164 plan.deps.insert(package.clone());
165 }
166 }
167 }
168
169 let src_root = out_dir.join("src");
171 fs::create_dir_all(&src_root)?;
172 let mut n_pkgs = 0usize;
173 let mut n_msgs = 0usize;
174 let mut n_srvs = 0usize;
175 for (pkg, plan) in &plans {
176 let pkg_dir = src_root.join(pkg);
177 let msgs: Vec<String> = plan.msgs.iter().cloned().collect();
178 let srvs: Vec<String> = plan.srvs.iter().cloned().collect();
179
180 if !msgs.is_empty() {
181 fs::create_dir_all(pkg_dir.join("msg"))?;
182 }
183 for m in &msgs {
184 let Some(path) = resolver.find_msg_path(pkg, m) else {
185 if verbose {
186 eprintln!("[robonix-codegen] ros2: missing .msg for {pkg}/{m}, skipping");
187 }
188 continue;
189 };
190 let raw =
191 fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
192 fs::write(
193 pkg_dir.join("msg").join(format!("{m}.msg")),
194 normalize_idl(&raw),
195 )?;
196 n_msgs += 1;
197 }
198
199 if !srvs.is_empty() {
200 fs::create_dir_all(pkg_dir.join("srv"))?;
201 }
202 for sv in &srvs {
203 let Some(path) = resolver.find_srv_path(pkg, sv) else {
204 if verbose {
205 eprintln!("[robonix-codegen] ros2: missing .srv for {pkg}/{sv}, skipping");
206 }
207 continue;
208 };
209 let raw =
210 fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
211 fs::write(
212 pkg_dir.join("srv").join(format!("{sv}.srv")),
213 normalize_idl(&raw),
214 )?;
215 n_srvs += 1;
216 }
217
218 fs::write(pkg_dir.join("package.xml"), package_xml(pkg, &plan.deps))?;
219 fs::write(
220 pkg_dir.join("CMakeLists.txt"),
221 cmakelists(pkg, &msgs, &srvs, &plan.deps),
222 )?;
223 n_pkgs += 1;
224 if verbose {
225 eprintln!(
226 "[robonix-codegen] ros2: {pkg} ({} msg, {} srv, deps: {})",
227 msgs.len(),
228 srvs.len(),
229 plan.deps.iter().cloned().collect::<Vec<_>>().join(" ")
230 );
231 }
232 }
233
234 eprintln!(
235 "[robonix-codegen] ros2: {n_pkgs} packages, {n_msgs} msg, {n_srvs} srv -> {}/src",
236 out_dir.display()
237 );
238 Ok(())
239}